TD 7 INF422 — Accès direct aux périphériques

Introduction      FAQ   Glossary   Project   TD 1   TD 2   TD 3   TD 4   TD 5   TD 6   TD 7   TD 8

Retour au cours.

Objectifs :

Le but de ce TD est de manipuler un périphérique d'Android, à la fois à partir du shell et à partir d'un programme Java. Ce périphérique, le frame-buffer, permet d'écrire directement dans la mémoire écran de l'ordinateur, pour effectuer toutes sortes d'opérations visuelles.

1. Manipulation du frame-buffer depuis le shell

Nous allons commencer par quelques manipulations simples du frame-buffer depuis le shell.

Le frame-buffer d'Android

Le frame-buffer d'Android peut être lu et écrit à partir du fichier périphérique /dev/graphics/fb0.

Il contient deux copies de l'écran, entre lesquelles le système alterne : ceci permet de modifier une copie pendant que l'autre est affichée, puis d'afficher d'un coup la copie modifiée. Cette méthode évite les effets visuels désagréables quand des modifications sont effectuées directement sur la copie visible.

La couleur de chaque pixel (point sur l'écran) est codé sur deux octets, et chaque ligne contient 320 pixels.

Comme d'habitude, nous allons lancer l'émulateur et nous y connecter.

emulator @inf422 &
adb forward tcp:4444 tcp:23
telnet 127.0.0.1 4444

Copier le contenu du fichier /dev/zero dans le frame-buffer. Que se passe-t'il ?

Copier le contenu du fichier /dev/urandom dans le frame-buffer. Que se passe-t'il ?

Appuyer sur la touche Menu du téléphone pour rétablir l'affichage.

Au cours de ce TD, le processus system_server va nous gêner légèrement. En effet, ce processus est en charge, entre autres, de :

Copier le contenu du frame-buffer dans un fichier /data/snap1.img. En déduire le nombre de lignes affichées à l'écran.

En appuyant sur diverses touches du téléphone, faire des copies du contenu du frame-buffer dans plusieurs fichiers pour des écrans différents.

Copier maintenant chacun de ces fichiers dans le frame-buffer et visionner les résultats.

Les signaux STOP et CONT

Deux signaux jouent un rôle très important dans la gestion des processus, et sont utilisés notoirement pas les shells :

Pour utiliser ces signaux, il suffit d'utiliser les commandes habituelles kill et killall avec en premier argument -STOP ou -CONT.

Utiliser les signaux STOP et CONT pour bloquer et débloquer alternativement le processus system_server.

Modifier le frame-buffer et vérifier que, quand system_server est bloqué, l'affichage de l'écran correspond exactement à celui du frame-buffer.

Télécharger les fichiers smiley.sh et colors.sh, et copier les sur Android :

adb push smiley.sh /home/smiley.sh
adb push colors.sh /home/colors.sh

Lancer les scripts /home/smiley.sh et /home/colors.sh dans Android.

2. Gestion des droits d'accès

Quels sont les droits d'accès attachés au périphérique frame-buffer ? Modifier ces droits si nécessaire pour que n'importe quel utilisateur puisse modifier le frame-buffer.

Droits associés aux programmes Java

Sous Android, les programmes Java ne sont pas exécutés par l'utilisateur root, mais par de faux utilisateurs. Ceci permet de restreindre leurs actions, et de diminuer les risques en matière de sécurité.

Par ailleurs, les programmes Java ne sont pas autorisés à lancer des commandes Unix sur Andoid.

Dans la suite du TD, il sera important que notre application Java soit capable de bloquer et débloquer le processus system_server. Cependant, l'envoi d'un signal a un processus n'est autorisé que s'il provient du même utilisateur que celui qui a lancé le processus, ou de root. L'application Java que nous allons écrire ne pourra donc pas directement interagir avec system_server.

Pour résoudre ce problème, nous allons utiliser l'architecture suivante:

Écrire un script shell /home/stop.sh qui bloque le processus system_server et ouvre l'accès en écriture au frame-buffer à tous les utilisateurs.

Écrire un script /home/cont.sh qui débloque le processus system_server et rétablit les droit d'accès originaux au frame-buffer.

Utiliser la commande nc pour lancer les deux serveurs. Modifier les deux scripts pour qu'une ligne ok soit retournée à chaque connexion.

3. Réseau et threads

Télécharger les sources de l'application TD7.

L'application TD7

Nous avons prédéfini un certain nombre de fonctions pour vous :

La fonction connect est utilisée pour interagir avec les deux serveurs mis en place dans la question précédente.

Compléter la fonction connect(port) pour :

La fonction mythread() contient le coeur du programme. Elle doit être exécutée dans un thread différent du thread principal, puisque ce dernier, qui a la charge de l'interface graphique Android, sera bloqué lorsqu'il interagit avec le processus system_server que l'on a stoppé au début de l'exécution.

Compléter la fonction onCreate() pour créer et exécuter un thread dont le seul rôle est d'appeler la fonction mythread().

4. Écriture dans le frame-buffer

La classe FileOutputStream

La classe FileOutputStream permet d'écrire des données directement dans un fichier. Ceci peut s'avérer utile si on ne veut pas que des conversions soient effectuées automatiquement (conversions entre codages différents suivant les pays par exemple).

Dans l'application TD7, nous avons défini certains paramètres de l'écran (dimensions, couleurs principales), ainsi qu'un tableau screen (de type byte[]) de la taille exacte de la mémoire écran.

Définir une fonction syncScreen() qui affiche le contenu de screen sur l'écran du téléphone. Pour cela, on copiera deux fois à la suite le contenu du tableau screen dans le frame-buffer.

Voir la correction

5. Primitives graphiques

Chaque pixel (point de l'écran) est représenté par deux octets dans le tableau screen.

Les différents types d'entiers

Java, comme de nombreux languages de programmation, définit plusieurs types d'entiers suivant le nombre d'octets utilisés pour les encoder, et les plages de valeurs qu'ils peuvent prendre:

Il est possible de changer le type d'un entier, si celui-ci est bien conforme à la plage de valeurs du nouveau type:

int x = 56;
char y = (char)x;

Définir une fonction colorPixel(int x, int y, int color) qui modifie les deux octets du pixel (x,y) pour lui donner la couleur color. On pourra utiliser l'opérateur modulo % ou le et binaire &.

Définir une fonction colorScreen(int color) qui remplit tout l'écran avec la même couleur color.

Chaque couleur est codée sur deux octets, au format RGB en 5:5:5:1 bits, le bit de poids faible n'étant pas utilisé.

Définir une fonction color(int red, int green, int blue) qui retourne un entier encodant une couleur, à partir de ses composantes red, green et blue sur 5 bits (i.e. dans [0:31]).

Tester cette fonction avec colorScreen en coloriant l'écran en rouge, vert, bleu, blanc, noir et jaune.

Définir une fonction colorLine(int x0, int y0, int x1, int y1, int color) qui colorie tous les points sur une ligne entre (x0,y0) et (x1,y1). On pourra utiliser des types float pour améliorer la qualité de la ligne.

6. Lecture d'un snapshot

Dans cette partie, nous allons lire un snapshot depuis un fichier et l'écrire dans le tableau screen.

La classe FileInputStream

La classe FileInputStream permet de lire des données directement depuis un fichier.

Le paramêtre len de la méthode read de FileInputStream n'indique pas le nombre exact d'octets lus, mais la longueur maximale lue. Il peut être pratique de disposer d'une fonction plus prévisible.

Définir une fonction reallyInput(FileInputStream in, byte[] bytes, int pos, int len) qui retourne uniquement quand len octets ont été lus depuis le fichier et placés dans bytes à la position pos.

Voir la correction

Nous pouvons maintenant utiliser cette fonction pour lire tout un écran à la fois.

Définir une fonction readSnapshot(String file_name) qui lit le premier écran placé dans le fichier file_name et le copie dans screen.

Utiliser cette fonction pour afficher le contenu du fichier /data/snap1.img.

7. Analyse du système de fichiers

Nous allons maintenant utiliser les primitives précédentes pour afficher des informations sur le système de fichiers. En particulier, nous serions intéressés par connaître la distribution des tailles des fichiers sur le système Android.

Pour cela, nous allons tracer sur l'écran la courbe suivante: en abscisse, les tailles des fichiers (on pourra commencer par une échelle de 1 pixel = 1 Koctect); en ordonnée, le pourcentage de fichiers dont la taille est inférieure à la valeur en abscisse. Ainsi, un point sur la courbe de coordonnées (x,y) indique que y% des fichiers ont une taille inférieure à x Ko. Ce type de courbe est appelée une Cumulative Distribution Function.

Définir une fonction readDirectory(String dir, int unit, int[] nfiles), dont les arguments sont le nom d'un répertoire, une unité d'échelle et un tableau d'entiers. Ce tableau d'entiers contient, pour chaque indice i, le nombre de fichiers trouvés dont la taille est comprise entre i*unit et (i+1)*unit. La fonction readDirectory doit mettre à jour ce tableau à partir du contenu du répertoire dir.

En appelant la fonction readDirectory à partir de la racine du système de fichier, nous pouvons maintenant collecter la distribution des tailles de fichiers.

Ecrire la fonction dist2CDF(int[] dist) qui transforme le tableau de distribution précédent en un tableau de CDF.

Nous avons maintenant obtenu le tableau qu'il nous reste à afficher sur l'écran du téléphone.

Ecrire la fonction displayCDF()qui affiche le tableau de la CDF sur l'écran. Interpréter le résultat.

Vous pouvez maintenant vous amuser à afficher d'autres statistiques sur le système de votre téléphone.