Connaissez-vous le point commun entre les séries TV Battlestar Galactica (1978) et K2000 (1982)? En fait il y en a au moins deux: leur créateur Glen A. Larson, ainsi que ce petit chenillard qui se promène sur la visière des Cylons et à l'avant de KITT, d'où l'expression "Larson scanner". On s'est demandé s'il était possible de faire la même chose avec les animations autonomes du Yocto-Color-V2.
"Personnages" emblématiques des séries K2000 et Battlestar Galactica, notez le chenillard rouge
Yocto-Color-V2 et Scanner de Larson
Évidement, reproduire cet effet lumineux en programmation pure avec un Yocto-Color-V2 est juste l'affaire de quelques lignes de code, mais cela implique d'avoir un ordinateur qui communique en permanence avec le module. En revanche, configurer un Yocto-Color-V2 pour qu'il le fasse de manière autonome est suffisamment subtil pour qu'on se dise que cela valait la peine d'un faire un article.
Pour rappel, Yocto-Color-V2 est capable de jouer des animations autonomes. Chacune de ces animations est définie comme une suite de variations de couleurs qui peut être jouée par n'importe laquelle des LEDs avec un décalage temporel prédéfini, vous trouverez plus de détails dans la doc du Yocto-Color-V2. C'est relativement simple et cela offre pas mal de possibilités. Il est même possible de faire en sorte que ces animations démarrent automatiquement à la mise sous tension du module, et du coup ça peut marcher même si le module n'est alimenté que par un chargeur ou une batterie USB.
Un Yocto-Color-V2 connecté à une barrette de 8 LEDs NEOPIXEL de Adafruit
Version simplifiée
La difficulté dans le cas du "Scanner de Larson", c'est ce mouvement de va-et-vient. Il se trouve qu'il est possible d'insérer un miroir dans une animation ce qui a pour effet d'inverser sa vitesse et donc de la faire repartir dans l'autre sens. On va évidement tirer parti de cette fonctionnalité pour faire faire des allers-et-retours à un seul pixel.
Mais avant tout, commençons par définir quelques constantes, soit
- LedCount, le nombre de LEDs à gérer, soit 8 dans notre cas
- halfPeriod, le temps en milli-secondes pour faire un aller simple
On note que le temps d'allumage d'une LED est donc de halfPeriod/LedCount. Pour éviter des artefacts liés aux problèmes d'arrondi dans le module, il est préférable que halfPeriod soit un multiple de LedCount.
La couleurs des LEDs va alterner entre deux couleurs HSL définies comme suit:
- H, la teinte de l'animation, on a choisi 0 pour garder le rouge historique
- S, la saturation de l'animation, on a choisi le maximum soit 255
- L, la luminosité, on a pris 127, le maximum qu'on puisse se permettre sans altérer la teinte
- Low = (H<<16) | (S<<8) pour les LEDs éteintes
- High = (H<<16) | (S<<8) | L pour les LEDs allumées
Notez que la valeur de Low qui correspond à noir n'est pas définie comme 0x000000 (noir aussi) mais comme 0xHHSS00 pour que les transitions HSL soient calculées correctement. Une fois ces bases posées, on construit notre animation, on a utilisé du Python, mais ça marche aussi dans tous les autres langages.
if leds is None: sys.exit("No color led cluster found")
leds.set_activeLedCount(LedCount);
leds.resetBlinkSeq(0) # reset sequence 0
# addHslMoveToBlinkSeq(sequence, color, duration)
leds.addHslMoveToBlinkSeq(0, High, halfPeriod /(2*LedCount)) # High
leds.addHslMoveToBlinkSeq(0, High, 0)
leds.addHslMoveToBlinkSeq(0, Low, 0) # low
leds.addHslMoveToBlinkSeq(0, Low, (LedCount-1)*halfPeriod /(LedCount))
leds.addHslMoveToBlinkSeq(0, Low, 0)
leds.addHslMoveToBlinkSeq(0, High, 0) # High again
leds.addHslMoveToBlinkSeq(0, High, halfPeriod /(2*LedCount)) # keep it
leds.addMirrorToBlinkSeq(0) # do the same thing the other way around
On a construit l'animation de façon parfaitement symétrique avec le rouge (high) à cheval sur les extrémités de l'animation pour éviter les artefacts qui peuvent apparaître sporadiquement sur les bords si on s'y prend autrement.
Enfin on affecte les LEDs à l'animation, chaque LED n est décalée de -n*(halfPeriod /LedCount) - (halfPeriod /(2*LedCount)) . Comme cette expression va en diminuant et qu'on ne peut pas utiliser un décalage négatif, on part de halfPeriod plutôt que zéro, ce qui revient au même puisque notre animation à une longueur de halfPeriod. La partie -(halfPeriod /(2*LedCount)) sert à compenser le fait que le rouge est à cheval sur les extrémités.
#linkLedToBlinkSeq(ledIndex,Count,Sequence,Offset)
leds.linkLedToBlinkSeq(i,1,0, halfPeriod-i*(halfPeriod /LedCount) \
- (halfPeriod /(2*LedCount))
leds.startBlinkSeq(0) # start the sequence
Et voilà le résultat:
Version simplifiée du Scanner de Larson
Ajouter un dégradé
Maintenant, pour améliorer le rendu on aimerait ajouter un dégradé à l'avant et l'arrière du pixel qui se promène, et c'est là que les choses se compliquent. Les animations du Yocto-Color-V2 sont par nature cycliques: tout ce qui dépasse d'un côté ré-apparaît de l'autre. Il faut donc trouver un moyen de tronquer.
La cyclicité des animations des Yocto-Color-V2 nous pose un problème de débordement sur la gauche
L'astuce est de créer une animation plus grande que le nombres de pixels qu'on veut utiliser afin de pouvoir exclure les réflexions indésirables.
Mais on peut tricher en créant une animation plus grande
Si on défini tail = 2 comme la largeur en pixel du dégradé, on modifie la longueur de l'animation
LedCount+= tail
Attention, il faut bien choisir le nombre de pixels qu'on ajoute à l'animation, trop grand l'animation va disparaître dans la partie masquée, trop petit les réflexions vont réapparaître. Agrandir d'une fois la longueur du dégradé semble un bon compromis
On construit l'animation en prenant soin de garder les temps bien symétriques
# addHslMoveToBlinkSeq(sequence, color, duration)
leds.addHslMoveToBlinkSeq(0, High, halfPeriod /(2*LedCount))
leds.addHslMoveToBlinkSeq(0, Low, 0.75*tail*halfPeriod /(LedCount))
leds.addHslMoveToBlinkSeq(0, Low, 0.25*tail*halfPeriod /(LedCount))
leds.addHslMoveToBlinkSeq(0, Low, (LedCount-2*tail)*halfPeriod /(LedCount))
leds.addHslMoveToBlinkSeq(0, Low, 0.25*tail*halfPeriod /(LedCount))
leds.addHslMoveToBlinkSeq(0, High, 0.75*tail* halfPeriod /(LedCount))
leds.addHslMoveToBlinkSeq(0, High, halfPeriod /(2*LedCount))
leds.addMirrorToBlinkSeq(0)
Le prix à payer de cette astuce est la complexité de l'affection de décalage.
#linkLedToBlinkSeq(ledIndex,Count,Sequence,Offset)
leds.linkLedToBlinkSeq(i,1,0, -(tail/4)*(halfPeriod /LedCount) \
+ halfPeriod-i*(halfPeriod /LedCount) \
- halfPeriod /(2*LedCount) )
leds.startBlinkSeq(0)
Et voilà le résultat:
Une version plus jolie
Vous trouverez le code source complet de ces deux animations, ainsi qu'une version synchrone en programmation pure, dans ce ficher ZIP.
Et maintenant?
La question qui se pose maintenant, c'est comment avoir le dégradé d'un seul côté, comme si le chenillard laissait une traînée derrière lui. On a longtemps cherché et on pense que ce n'est pas possible avec le système d'animations autonomes du Yocto-Color-V2 et ce, malgré l'existence des Jump qui permettent à une LED de passer d'une séquence à l'autre. Mais pour être francs, on n'en est pas complètement sûrs....
On aurait bien aimé arriver à faire ça