20 Mar 21

T-Watch 2020 : un script Python pour automatiser les captures d'écran

Catégorie : Hack
Étiquettes :

[le script est téléchargeable en fin d'article]

Pour illustrer des tutoriels pour la T-Watch, il est intéressant de pouvoir générer facilement des captures d'écran de la montre. En regardant dans la documentation du firmware de sharandac sur github, on peut y lire que deux méthodes sont possibles pour faire des captures d'écran.

Tout d'abord, le bouton poussoir de la montre peut être programmé pour qu'avec un appui de deux secondes, cela fasse automatiquement une capture d'écran. On télécharge ensuite cette capture en passant par le serveur FTP intégré au firmware s'il est activé.

L'autre façon est d'utiliser le serveur web intégré. Il faut dans un premier temps déclencher la prise de la capture d'écran, avec la commande bash suivante :

wget xxx.xxx.xxx.xxx/shot

où xxx.xxx.xxx.xxx est l'adresse IP de la montre. Puis on récupère la capture d'écran avec la commande :

wget xxxx.xxxx.xxxx.xxxx/screen.data

(au passage, la documentation indique screen.565 au lieu de screen.data mais j'ai signalé l'erreur).

Quelle que soit la méthode, le fichier contenant la capture d'écran s'appelle screen.data, correspond à une image de 240×240 pixels, au format RGB565, et on nous indique qu'on peut la lire avec Gimp. Bon, ça fonctionne, mais ce n'est pas pratique du tout si on doit faire beaucoup de captures d'écran.

L'idée de ce billet est de montrer comment on peut avec un petit script Python automatiser la récupération de la capture d'écran directement dans le format de son choix (png, jpeg, webp…)

Le format RGB565

Il s'agit d'un format d'image où chaque pixel est codé sur deux octets seulement (16 bits) au lieu de trois généralement (un octet pour chaque couleur rouge/vert/bleu qui est donc codée de 0 à 255, soit au total 256 × 256 × 256 = 16 millions de couleurs). Ce format particulier est lié à l'utilisation de l'écran ST7789V. Dans ce format, il n'y a donc que 256*256 = 65536 couleurs différentes.

Il faut donc coder les trois composantes rouge/vert/bleu de chaque pixel sur deux octets, soit 16 bits :

  • le rouge sera codé sur 5 bits seulement, ce qui fait seulement 2^5 = 32 niveaux de rouge. Comme le rouge est au final interprété comme un nombre entier entre 0 et 255, cela revient à n'autoriser que les multiples de 8 : 0, 7, 15…
  • le vert est lui codé sur 6 bits, ce qui fait 64 niveaux de vert. Ici, cela revient à n'autoriser que des multiples de 4 pour ce canal.
  • enfin, le bleu est tout comme le rouge codé sur 5 bits, avec donc la même résolution. D'où le nom du format : RGB565.

Le choix d'une résolution double pour le canal vert est peut-être lié au fait que la sensibilité de l'œil est maximale dans cette bande de longueurs d'onde par rapport au bleu et au rouge. Je n'ai pas trouvé de source en ce sens pour l'instant.

Pour chaque pixel de l'image, les 16 bits sont donc organisés de la façon suivante :

Pour retrouver le code couleur rouge par exemple, il faut donc isoler les cinq premiers bits du premier octet, et y ajouter trois zéros à droite (bits r2, r1 et r0 manquants, et faisant donc qu'on n'a que des multiples de 8) pour obtenir un entier entre 0 et 255. La méthode est la même pour le code couleur du vert, puis du bleu.

Le fichier image ne comporte aucune en-tête, qui indiquerait entre autre la largeur et la hauteur de l'image. On a directement les 240×240×2 = 115200 octets.

Le script Python

Commençons d'abord par gérer les paramètres qui peuvent être passés au script. Le plus simple serait d'appeler le script en passant l'adresse IP de la montre en paramètre. On ajoute également comme paramètre optionnel le nom du fichier image. Ça a l'intérêt d'indiquer dans quel format on veut la capture, simplement par l'extension du fichier. Si ce paramètre optionnel n'est pas indiqué, on choisira un nom par défaut (et donc un format par défaut).

Pour gérer les paramètres en ligne de commande, j'ai choisi d'utiliser la bibliothèque argparse

import argparse

parser = argparse.ArgumentParser(description='Make a screenshot from T-Watch')
parser.add_argument('ip', nargs=1, help="IP of the T-Watch")
parser.add_argument('-output', help="filename of the screenshot", default="t-watch_screenshot.png")

La première ligne crée l'analyseur (parseur), en indiquant au passage ce que fait le script. On crée ensuite le paramètre ip, obligatoire, avec l'aide correspondante. Enfin, on crée le paramètre output (optionnel car commence par un tiret), avec valeur par défaut t-watch_screenshot.png si ce paramètre n'est pas fourni.

Si on appelle le script avec le paramètre --help, on a alors la minidocumentation associée :

usage: twatch_screenshot.py [-h] [-output OUTPUT] ip

Make a screenshot from T-Watch

positional arguments:
  ip              IP of the T-Watch

optional arguments:
  -h, --help      show this help message and exit
  -output OUTPUT  filename of the screenshot

Pratique, non ?

Passons maintenant à la partie génération et récupération de la capture. Comme vu précédemment, il suffit d'interroger le serveur web de la montre une fois avec l'url /shot, puis avec /screen.data. On utilisera donc les lignes suivantes :

import requests
url = f"http://{args.ip[0]}/shot"
r = requests.get(url)
url = f"http://{args.ip[0]}/screen.data"
r = requests.get(url)
dataArray = r.content

args.ip[0] correspond à l'adresse IP passée en paramètre du script.

Les données de la capture d'écran se trouvent alors dans la variable dataArray

Nous créons ensuite une matrice numpy de 240 lignes et 240 colonnes, chaque cellule de la matrice contenant trois nombre entiers non signés sur 8 bits (uint8) correspondant aux codes rouge/vert/bleu de chaque pixel.

line = 240
column = 240
img = np.array([[[0] * 3] * column] * line, dtype=np.uint8)

Nous créons ensuite une boucle pour chaque pixel de notre capture au format RGB565, et nous récupérons les deux octets contenant les trois codes couleurs :

for x in range (0,line):
    for y in range (0,column):
         index = 2*y+2*column*x
         pixel = dataArray[index]<<8|dataArray[index+1]

La dernière ligne permet de concaténer les deux octets en un seul mot de 16 bits correspondant au schéma montré plus haut. On isole ensuite les bits correspondant à chaque canal de couleurs en utilisant des masques et l'opérateur & (et) :

R = pixel&0b1111100000000000
G = pixel&0b0000011111100000
B = pixel&0b0000000000011111

Il faut maintenant décaler le code rouge 8 fois vers la droite (pour éliminer les 8 zéros en trop et garder les 3 zéros qui vont compléter les 5 bits de données). De la même façon, on décalera le code vert de 3 bits vers la droite, et le code bleu de trois bits vers la gauche (pour ajouter les 3 zéros manquant à droite). Ces trois valeurs sont stockées dans la matrice image (ensemble de trois octets aux coordonnées x et y)

img[x,y,0] = R>>8
img[x,y,1] = G>>3
img[x,y,2] = B<<3

Il ne reste plus qu'à convertir la matrice img en véritable image. Pour cela, nous utilisons la classe Image de la bibliothèque PIL (Python Imaging Library).

from PIL import Image
new_image = Image.fromarray(img,"RGB")
new_image.save(args.output)

Et c'est tout ! Il suffit ensuite d'appeler le script avec une commande du style :

python twatch_screenshot.py 192.168.1.42 -output=capture.png

Le script Python complet est téléchargeable via le lien ci-dessous. C'est sous licence libre GPL. Enjoy !

Télécharger le script