Hello all,
aujourd'hui je ne viens pas vous poser une question mais vous proposer le write-up d'un petit projet que j'ai réalisé pour un pote qui tient un bar.
Vous avez peut-être remarqué, mais dans les bars il est de plus en plus courrant d'avoir un afficheur des "likes" facebook de la page du bar. Apparement, ça aiderait bien les clients à liker la page devant l'engin, pour voir s'il fonctionne vraiment ( Ahhhh, le social engineering

). Par contre, ce genre d'objet coute quand même relativement cher (je vous laisser regarder les prix de smiirl, c'est quand même pas donné).
Je vous propose donc de regarder comment on a créé le notre pour un peu moins de 30€.
Je tiens à préciser que je ne connais presque rien à l'électronique ni a l'embarqué en fait. Je suis parti de rien et ai fini par le faire fonctionner juste avec quelques recherches et de la documentation. Si certains points vous semblent bizarres ou maladroits, c'est donc normal.Le matérielPour le choix du matériel, ça a été vite fait. Vu qu'il va falloire communiquer avec un serveur pour récupérer le nombre de likes (et que je ne maitrise vraiment pas cette partie la des arduino, puis un shield ça coute cher), on est parti sur un raspberry pi. Et vu qu'on a vraiment pas besoin d'une grosse puissance, on a pris un raspi zero.
Pour l'affichage, on est parti sur une solution assez simple: un chainage d'afficheurs de 8x8 leds (des max7219 pour être précis).
Je mets les liens ci-dessous à titre informatif, mais je vous recommande quand même cette boutique, c'est là qu'on a acheté le matos

- Le raspi:
https://www.kubii.fr/pi-zero-v13/1401-raspberry-pi-zero-v13-kubii-3272496006973.html- L'adaptateur GPIO:
https://www.kubii.fr/raspberry-pi/1603-adaptateur-gpio-2x20-male-pour-pi-zero-kubii-3272496004030.html- Une carte microSd:
https://www.kubii.fr/pi-zero-v13/1884-carte-microsd-4gb-avec-noobs-pour-pi-zero-kubii-3272496007338.html- L'afficheur:
https://www.kubii.fr/ecrans-afficheurs/1950-max7219-module-de-commande-d-affichage-a-matrice-de-points-4-en-1-pour-arduino-kubii-3272496008083.html PréparationBon, on a reçu le matos, et a part un petit faux contact sur l'afficheur, tout fonctionne correctement.
On a donc installé un raspbian sur la SD, vérfié que ça fonctionne puis connecté le raspi au wifi.
Afin de travailler sur un PC plus puissant que le raspi (et surtout parce que j'avais pas de clavier a disposition en fait), j'y ai activé un serveur ssh pour prendre la main depuis mon laptop.
Après quelques recherches sur l'afficheur, on voit qu'il fonctionne avec
un bus SPI. Je vous laisser aussi le lien vers la
fiche technique de l'afficheur qui est très utile pour avoir la liste des commandes à disposition (sur la page 7).
Ça tombe vraiment bien: linux intégre un driver SPI, et il est même installé par défault sur raspbian, il suffit de l'activer comme
décrit ici.
De plus, j'ai trouvé cette lib python
luma.led_matrix qui a l'air de gérer les max7219 et intégre des outils vraiment sympa pour directement afficher des fonts ou des images dessus. Au final on ne va pas tant se servir que ça de la gestion d'images (on va juste afficher des chiffres). Pour l'installer je me suis contenté de suivre
leur documentationLe wiringBon, désole j'ai pas pris de photos à cette étape là

vous devrez donc vous contenter d'un schema.
Cette partie était franchement la plus facile, le bus SPI du max7219 n'utilise que 5 fils:
- le VCC: entrée 5v
- le GND: la masse
- le DIN: le bus de données
- le CS: chip select ... j'ai pas vraiment compris pourquoi il s'appel comme ça, mais en gros, quand on veut envoyer des données sur le DIN, il faut d'abord créer faire un front descendant sur cette pin, envoyer les données sur le DIN, puis faire un front montant sur lui. Heureusement grace a luma.led_matrix on aura pas à gérer ça

- le CLK: l'horloge. Sur notre composant elle ne doit pas dépasser les 10MHz, mais de toute façon c'est le driver linux qui la gère celle là. (et luma.led_matrix qui défini la cadence)
En suivant les indications de la documentation de luma.led_matrix, et avec
cette jolie représentation des pins, le branchement est on ne peut plus facile:
https://framapic.org/J2XEKnrBc0F3/n84Ut5GwKcWB.jpgLes premiers tests de programmationBon, depuis tout à l'heure je vous parle de luma.led_matrix. C'est une librairie python, et maintenant qu'il est temps de coder ... bah on va faire du python

Tout d'abord, il va nous falloir comprendre comme ça fonctionne tout ça. En se basant sur la documentation du composant et les exemples de la librairie, j'en suis venu à cette conclusion:
- Les commandes son codées sur 2 octets: le 1er est l'addresse du registre a modifier, le second en est la valeur.
- Les registres de 0x1 à 0x8 sont ceux a utiliser pour allumer ou éteindre les leds d'un afficheur 8x8. Ils correspondent aux lignes de led: le registre 0x1 est la ligne la plus haute, le 0x8 est la ligne tout en bas. Chaque bit des valeurs de ces registres correpondent a une led. Le bit de poids faible étant la led la plus à droite de la ligne, le bit de poids celui de gauche. Par exemple, si on met 0xef dans le registre 0x1, toutes les leds SAUF celle de gauche seront allumées.
- Quand on chaine plusieurs max7219, il faut modifier le même registre plusieurs fois d'affilé pour que les instructions passent à l'afficheur suivant. Par exemple si on envoie la commande suivante: b'\x01\xff\x01\x0f', l'afficheur connecté directement au raspberry aura les 4 leds de droite de sa premiere ligne allumée, tandis que l'afficheur suivant aura toutes celles de sa premiere ligne allumée. Dans mon cas, l'afficheur connecté au raspberry est celui de droite, donc il faut envoyer les commandes en prenant en compte que le dernier afficheur est celui de droite.
Le premier test à donc été d'afficher des motifs bidons pour vérifier que tout fonctionne:
#!/usr/bin/env python3
from luma.led_matrix.device import max7219
from luma.core.interface.seriunal import spi, noop
import time
# on crée le serial
serial = spi(port=0, device=0, gpio=noop())
# bon, on se sert pas directement de cette classe, mais elle permet d'initialiser correctement le serial pour gérer le chainage des 4 max7219
max7219(serial, cascaded=4)
# On calme un peu la luminosité sur les 4 max7219
serial.data(4 * b'\x0a\x00')
# On envoie des motifs bidons:
serial.data(b'\x01\xff\x01\x0f\x01\xf0\x01\x33')
serial.data(b'\x03\xff\x03\x0f\x03\xf0\x03\x33')
serial.data(b'\x05\xff\x05\x0f\x05\xf0\x05\x33')
serial.data(b'\x07\xff\x07\x0f\x07\xf0\x07\x33')
time.sleep(10)
ce qui nous donne (oui j'ai toujours pas pris de photo désolé):
https://framapic.org/DiZPWdnPbUuM/YoRL7xvfja3F.jpgL'affichage de chiffresBon ... on sait maitenant controler l'afficheur, il faut maitenant qu'on arrive à afficher des chiffres.
Pour ça je ne me suis pas trop embêté: j'ai trouvé une font compatible avec une grille de 8x8, et j'ai récupéré les entrées pour les chiffres. (je n'arrive pas a retourver le lien où je l'ai trouvé désolé. Si ça me revient je vous le donnerai)
Il se trouve que cette font ne s'affichait pas dans le bon sens, j'ai donc juste appliqué une rotation de -90°. Ce qui nous donne:
CHARS = {
'0': [0x7c, 0xc6, 0xce, 0xde, 0xf6, 0xe6, 0x7c, 0x00],
'1': [0x30, 0x70, 0x30, 0x30, 0x30, 0x30, 0xfc, 0x00],
'2': [0x78, 0xcc, 0x0c, 0x38, 0x60, 0xcc, 0xfc, 0x00],
'3': [0x78, 0xcc, 0x0c, 0x38, 0x0c, 0xcc, 0x78, 0x00],
'4': [0x1c, 0x3c, 0x6c, 0xcc, 0xfe, 0x0c, 0x1e, 0x00],
'5': [0xfc, 0xc0, 0xf8, 0x0c, 0x0c, 0xcc, 0x78, 0x00],
'6': [0x38, 0x60, 0xc0, 0xf8, 0xcc, 0xcc, 0x78, 0x00],
'7': [0xfc, 0xcc, 0x0c, 0x18, 0x30, 0x30, 0x30, 0x00],
'8': [0x78, 0xcc, 0xcc, 0x78, 0xcc, 0xcc, 0x78, 0x00],
'9': [0x78, 0xcc, 0xcc, 0x7c, 0x0c, 0x18, 0x70, 0x00],
}
Il faut ensuite créer les chaines de commandes à envoyer sur le bus SPI pour afficher ces chiffres:
def buildDisplayCharCommands(char):
"""
Cette fonction construit une liste de commandes SPI pour afficher ce nombre.
Par exemple, pour char == '0', elle retourne:
[
b'\x01\x7c',
b'\x02\xc6',
b'\x03\xce',
b'\x04\xde',
b'\x05\xf6',
b'\x06\xe6',
b'\x07\x7c',
b'\x08\x00',
]
"""
if char not in CHARS:
return None
i = 1
res = []
for line in CHARS[char]:
res.append(bytes([i, line]))
i += 1
return res
def draw(serial, chars):
"""
Cette fonction va construire et envoyer les commandes pour afficher chaque ligne.
Par exemple pour chars == '1234', il va executer:
serial.data(b'\x01\x30\x01\x78\x01\x78\x01\x1c')
serial.data(b'\x02\xc6\x02\xcc\x02\xcc\x02\x3c')
...
"""
d = list(filter(None, map(buildDisplayCharCommands, chars)))
for i in range(len(d[0])):
cmd = b''.join(map(lambda x: x[i], d))
serial.data(cmd)
Récupérer le nombre de likes d'une page facebookBon... Pour cette étape, je ne vais pas tout détailler. En effet, c'est pas facile de comprendre les documentations de facebook pour ça (il suffit de chercher "comment avoir le nombre de likes d'une page facebook via api" pour voir que tout le monde en souffre

).
En fait, c'est pas si compliqué (sauf quand on a pas de compte facebook comme moi '--):
- Il faut créer une application sur
https://developers.facebook.com- Aller sur l'explorateur de l'API graph
- À coté du champ "token", il y a un dropdown, il faut sélectionner "Token d'accès de page", puis pendant la création, selection la page sur laquelle on veut l'info.
Voilà, on a un token qui va nous permettre de récupérer l'info qu'on veut.
En python, ça donne ça:
#!/usr/bin/env python3
import urllib.request
import json
def getNbLikes():
# ici on utilise /me car le token appartient à la page où on veut récuperer les likes
req = urllib.request.Request('https://graph.facebook.com/me?fields=fan_count', None, {
'Authorization': 'Bearer <leToken>'
})
try:
res = urllib.request.urlopen(req).read()
data = json.loads(res.decode())
return data['fan_count']
except:
return '0000'
On assemble toutAu final, le code qui nous permet d'afficher ce qu'on veut est:
#!/usr/bin/env python3
import time
import urllib.request
import json
from luma.led_matrix.device import max7219
from luma.core.interface.serial import spi, noop
def getNbLikes():
req = urllib.request.Request('https://graph.facebook.com/me?fields=fan_count', None, {
'Authorization': 'Bearer <leToken>'
})
try:
res = urllib.request.urlopen(req).read()
data = json.loads(res.decode())
return data['fan_count']
except:
return '0000'
CHARS = {
'0': [0x7c, 0xc6, 0xce, 0xde, 0xf6, 0xe6, 0x7c, 0x00],
'1': [0x30, 0x70, 0x30, 0x30, 0x30, 0x30, 0xfc, 0x00],
'2': [0x78, 0xcc, 0x0c, 0x38, 0x60, 0xcc, 0xfc, 0x00],
'3': [0x78, 0xcc, 0x0c, 0x38, 0x0c, 0xcc, 0x78, 0x00],
'4': [0x1c, 0x3c, 0x6c, 0xcc, 0xfe, 0x0c, 0x1e, 0x00],
'5': [0xfc, 0xc0, 0xf8, 0x0c, 0x0c, 0xcc, 0x78, 0x00],
'6': [0x38, 0x60, 0xc0, 0xf8, 0xcc, 0xcc, 0x78, 0x00],
'7': [0xfc, 0xcc, 0x0c, 0x18, 0x30, 0x30, 0x30, 0x00],
'8': [0x78, 0xcc, 0xcc, 0x78, 0xcc, 0xcc, 0x78, 0x00],
'9': [0x78, 0xcc, 0xcc, 0x7c, 0x0c, 0x18, 0x70, 0x00],
' ': [0, 0, 0, 0, 0, 0, 0, 0],
}
def buildDisplayCharCommands(char):
"""
Cette fonction construit une liste de commandes SPI pour afficher ce nombre.
Par exemple, pour char == '0', elle retourne:
[
b'\x01\x7c',
b'\x02\xc6',
b'\x03\xce',
b'\x04\xde',
b'\x05\xf6',
b'\x06\xe6',
b'\x07\x7c',
b'\x08\x00',
]
"""
if char not in CHARS:
return None
i = 1
res = []
for line in CHARS[char]:
res.append(bytes([i, line]))
i += 1
return res
def draw(serial, chars):
"""
Cette fonction va construire et envoyer les commandes pour afficher chaque ligne.
Par exemple pour chars == '1234', il va executer:
serial.data(b'\x01\x30\x01\x78\x01\x78\x01\x1c')
serial.data(b'\x02\xc6\x02\xcc\x02\xcc\x02\x3c')
...
"""
d = list(filter(None, map(buildDisplayCharCommands, chars)))
for i in range(len(d[0])):
cmd = b''.join(map(lambda x: x[i], d))
serial.data(cmd)
if __name__ == "__main__":
try:
# on crée le serial
serial = spi(port=0, serial=0, gpio=noop())
# bon, on se sert pas directement de cette classe, mais elle permet d'initialiser correctement le serial pour gérer le chainage des 4 max7219
max7219(serial, cascaded=4)
# On calme un peu la luminosité sur les 4 max7219
serial.data(4 * b'\x0a\x00')
# On fetch le nombre de like toutes les 5 secondes et on l'affiche.
while True:
draw(serial, str(getNbLikes()))
time.sleep(5)
except KeyboardInterrupt:
pass
Le petit truc de la finBon, on a un afficheur qui fonctionne ... Maitenant ça serait encore mieux qu'il fonctionne directement au démarrage n'est-ce pas ? (bah oui, faut lancer le script pour que l'affichage se fasse).
Pour ça, rien de plus simple: sur le raspberry pi, on lance `crontab -e`. On ajoute à la fin du fichier:
@reboot python3 /chemin/vers/le/script.py
Et on a fini ! Et cette fois ci j'ai une photo:
https://framapic.org/ZXkvfAwKkXnb/Pigw0stPonc6.jpg
EDIT: Bien sûr ce projet n'a rien d'exceptionnel, mais je me suis vraiment bien amusé et j'ai appris plein de trucs en le faisant. J'espère que vous viendrez nous présenter les votre par la suite.