Ici en Suisse, il existe un risque de pénurie d'énergie pour l'hiver prochain, et il n'est pas exclu que l'on soit confronté à des pannes d'électricité agendées ou non. C'est l'occasion de fabriquer un petit gadget pour nous aider à gérer les conséquences d’éventuelles pannes de courant.
Chez Yoctopuce, beaucoup de tâches sont automatisées à l'aide d'ordinateurs éparpillés un peu partout, et après une panne de courant il faut les rallumer un par un sans en oublier. Le projet de cette semaine est donc de construire un petit boîtier qui indiquera l'état de chaque machine, et si en plus le boîtier permettait de lancer un shutdown propre sur toutes des machines, cela permettrait d'éteindre facilement nos machines avant les coupures de courant annoncées.
Principe
La partie matérielle est juste triviale dans la mesure où on a juste besoin d'un panneau de LEDs et d'un gros bouton, il nous faut donc
- Un YoctoHub-Ethernet pour la gestion réseau.
- Un Yocto-Color-V2 et des barrettes de LED RGB, chaque LED correspondant l'état d'une machine
- Un bon gros commutateur connecté à un Yocto-Knob pour déclencher le shutdown des machines (note pour plus tard: la prochaine fois, ne pas oublier de commander les deux accessoires indispensables qui vont avec le commutateur)
Schéma de principe
Le principe consiste à faire tourner sur chaque machine un petit script qui prend le contrôle de la boîte et allume en vert la LED qui lui correspond. Si pour une raison ou une autre le script s'arrête, la même LED passe automatiquement en rouge. Si le script détecte que le commutateur de la boîte a été enclenché, il passe la LED en orange et lance un shutdown de la machine. Une fois que la machine est éteinte la LED s’éteint aussi.
La boîte en question
Programmation
La partie un peu compliquée de ce projet est la programmation du script, il y a différents aspects qui doivent être gérés correctement. On a décidé de faire une première version de ce script en Python parce que c'est un langage qui est facilement disponible tant sous Windows que Linux et Mac OS.
Gestion des connexions
Le YoctoHub-Ethernet ne peut gérer qu'un nombre limité de connexions parallèles. Le script doit donc se connecter sur la boîte, faire ce qu'il a à faire le plus vite possible avant de fermer la connexion, puis attendre un temps plus ou moins aléatoire avant de recommencer. Attendre un temps aléatoire permet d'éviter que toutes les machines n'essayent de se connecter à la boîte en même temps.
La structure du code ressemble donc à ceci:
if (YAPI.TestHub(CONTROLER_ADDR, 1000, errmsg) != YAPI.SUCCESS):
print("cannot reach " + CONTROLER_ADDR)
YAPI.FreeAPI()
else:
try:
if (YAPI.RegisterHub(CONTROLER_ADDR, errmsg) == YAPI.SUCCESS):
## payload
YAPI.FreeAPI()
except Exception as e:
print("Error " + str(e))
t = random.randint(SLEEP_AVG - 10, SLEEP_AVG + 10)
print("sleeping for " + str(t) + "sec")
time.sleep(t)
Configuration du système
Il faut stocker quelque part quelle machine correspond à quelle LED. On pourrait fournir ce paramètre à chaque instance du script grâce à la ligne commande, mais il nous a paru plus judicieux de centraliser cette information. Il se trouve que le YoctoHub-Ethernet contient un petit système de fichiers dans lequel on peut sauver un fichier de configuration. On a choisi un format archi-simple où chaque ligne correspond à une machine sous la forme LED index=Addresse IP, par exemple:
1=192.168.1.43 2=192.168.1.72 3=192.168.1.88
Comme on aimerait éviter de relancer les scripts après chaque changement de config tout en ne parsant le fichier de config que quand c'est nécessaire, pour cela on utilise le fait que le YoctoHub-Ethernet peut calculer le CRC de chaque fichier pour détecter les changements.
files = YFiles.FirstFiles()
if files is None:
sys.exit("No Files on " + CONTROLER_ADDR)
fileslist = files.get_list(CONFIG_FILE)
if len(fileslist) > 0:
if (fileslist[0].get_name() == CONFIG_FILE and fileslist[0].get_crc() != lastCRC ):
ledIndex = -1
lastCRC = fileslist[0].get_crc()
config = files.download(CONFIG_FILE).decode('utf-8')
lines = config.split()
for line in lines:
tokens = line.split("=")
if len(tokens) == 2:
if tokens[1] == myIPAddr:
ledIndex = int(tokens[0])
if (ledIndex < 0):
print("No Ledindex for " + myIPAddr + " in config.txt on " + CONTROLER_ADDR)
else:
print("LED index=" + str(ledIndex))
else:
print("No config.txt file on " + CONTROLER_ADDR)
Notez que sur les machines pourvues de plusieurs adaptateurs réseau, nous ne savez pas trop quelle adresse IP votre script va détecter en premier. Ce n'est pas un gros problème dans la mesure où rien ne vous empêche de définir plusieurs lignes pour la même LED dans le fichier de configuration. Du moment que toutes ces adresses correspondent à la même machine, ça marchera.
Gestion des LEDs
Le but principal du script est de faire passer en vert la LED qui lui correspond sur la façade de la boîte, mais on veut aussi que cette LED passe automatiquement au rouge si le script s’arrête, typiquement parce que la machine s'est éteinte de manière impromptue. Similairement, lorsque que le script détecte une demande de shutdown, le LED passe à l'orange puis s’éteint automatiquement quand le script s'est arrêté parce que la machine s'est effectivement éteinte.
Pour faire en sorte qu'une led change automatiquement de couleur lorsque que le script qui la contrôle s'est arrêté, on ré-utilise l'astuce des animations du Yocto-Color. Elle utilise le fait que les animations du Yocto-Color tournent en boucle, à moins qu'elles ne contiennent une instruction de saut qui permet d'enchaîner sur une autre animation. Elle consiste à pré-programmer deux animations:
- La première maintient la première couleur, par exemple vert, pendant un temps prédéterminé, puis saute à la deuxième animation
- La seconde animation maintient la deuxième couleur, par exemple rouge, en boucle
Tant que le script tourne, la LED est périodiquement réaffectée à la première animation avant qu'elle n'ait le temps de sauter à la deuxième animation. Si le script s'arrête, la LED finira par enchaîner sur la deuxième animation et changera de couleur toute seule.
Il est possible de définir les animations une bonne fois pour toutes et les sauver sur le système de ficher du Yocto-Color. Mais on a utilisé une approche un peu différente: le script vérifie au démarrage si les animations sont déjà définie dans la mémoire du Yocto-Color et les crée au besoin. Ainsi, si on souhaite changer les animations, il suffit de changer le script et de redémarrer le module puis le script pour que les animations soient mises à jour.
En pendant qu'on y est, l’initialisation définit, si ce n'est pas déjà fait, la couleur de démarrage de la LED à rouge. Ainsi, lorsque le courant reviendra après une panne, toutes les LEDS utilisées seront rouges par défaut.
if cluster is None:
sys.exit("No ColorLedCluster on " + CONTROLER_ADDR)
if FirstTime:
signatures = cluster.get_blinkSeqSignatures(0, 5)
if (signatures[GREENSEQ] == 0):
cluster.resetBlinkSeq(GREENSEQ)
cluster.addRgbMoveToBlinkSeq(GREENSEQ, GREEN, 0)
cluster.addRgbMoveToBlinkSeq(GREENSEQ, GREEN, DELAY)
cluster.addJumpToBlinkSeq(GREENSEQ, REDSEQ)
cluster.startBlinkSeq(GREENSEQ)
if (signatures[REDSEQ] == 0):
cluster.resetBlinkSeq(REDSEQ)
cluster.addRgbMoveToBlinkSeq(REDSEQ, RED, 0)
cluster.addRgbMoveToBlinkSeq(REDSEQ, RED, 10000)
cluster.startBlinkSeq(REDSEQ)
## define other sequences ...
if cluster.get_rgbColorArrayAtPowerOn(ledIndex, 1)[0] != RED:
cluster.set_rgbColorAtPowerOn(ledIndex, 1, RED)
cluster.saveLedsConfigAtPowerOn()
Gestion du shutdown
La gestion du shutdown consiste d'abord à vérifier que le commutateur n'est pas déjà enclenché au démarrage du script pour éviter qu'une machine ne se ré-éteigne à peine allumée parce qu'on a oublié de remettre le commutateur en place. Une fois la commande de shutdown envoyée au système, le script continue à tourner en maintenant la LED à l'orange, on compte sur le processus de shutdown pour le tuer. Une fois le script mort, la LED s'éteindra grâce au mécanisme d'animations. Notez que le script aura probablement besoin de privilèges adéquats pour pouvoir lancer la commande de shutdown.
if (ShutDownAlreadySetAtStartUp):
print("Switch was left in shutdown position")
cluster.linkLedToBlinkSeq(ledIndex, 1, ORANGEBLINKSEQ, 0)
else:
cluster.linkLedToBlinkSeq(ledIndex, 1, ORANGESEQ, 0)
if ShutdownAlreadySent:
print("Shutdown in progress...")
else:
print("Initialing shutdown...")
platformName = platform.system();
#platformName = "DISABLE SHUTDOWN"
if platformName == "Windows":
os.system('shutdown -s')
elif platformName == "Linux" :
os.system('systemctl poweroff')
elif platformName == "Darwin": #Mac
os.system("shutdown -h now") # might not work if not root
else:
print("Unknown platform : " + platformName)
ShutdownAlreadySent = True
Conclusion
On a un peu fait le tour des points importants à propos de cette petite application. Si vous voulez en savoir plus, vous pouvez toujours télécharger le code source complet ici.
On est prêt
A l'occasion, on pourra réécrire le script en C++ pour en faire un service Windows, ce qui sera probablement plus élégant que d'avoir une fenêtre DOS ouverte en permanence sur les machines Windows gérées par le système.
Notez que si la boîte permet d'éteindre les machines à distance, elle ne permet pas de les rallumer. On aura peut-être l'occasion d'en reparler :-)