3615 TuVeuxMaPhoto?

Minitel ? Késako ?

Le Minitel (Médium interactif par numérisation d’information téléphonique) est un type de terminal informatique destiné à la connexion au service français de Vidéotex baptisé Télétel, commercialement exploité en France entre 1980 et 2012.

source : https://fr.wikipedia.org/wiki/Minitel

L’ensemble Minitel+Télétel était en quelque sorte l’ancêtre du Web actuel !

 

Le réseau Télétel du Minitel était accessible par différents numéros courts, dont le plus populaire était le 3615.

Il permettait l’accès, payant (60 francs – 9,15 € – par heure environ, payés par l’usager, dont 40 F – 6,10 € – pour le service et 20 F – 3,05 € – pour France Télécom), à de très nombreux services (jusqu’à 25 000 en 1996 !) :

  • presse écrite
  • jeux
  • vente par correspondance
  • SNCF
  • banques
  • rencontres
  • et même des recherches d’emploi :

Pour en savoir plus sur le Minitel : https://larevuedesmedias.ina.fr/du-minitel-linternet

 

L’objectif de ce mini projet est de redonner vie à cet objet mythique de l’histoire de l’informatique et des communications !

 


Utilisation du Minitel avec un Arduino

Le Minitel permet la transmission de données par voie série au moyen d’un connecteur DIN situé à l’arrière, avec des débits pouvant atteindre 4 800 bits/s, et même 9 600 bits/s avec le Minitel 2.

Rappel : l’unité bits/s se confond ici avec le baud.

  • 2 : masse (GND)
  • 3 : émission de données (Tx)
  • 1 : réception de données (Rx)

 

A l’aide d’un microcontrôleur Arduino, on peut communiquer avec le minitel via le protocole série RS-232.

 

 

Coté écran, on peut bien sûr afficher du texte (encodage spécial dérivé de l’ASCII), mais il existe également un mode « semi-graphique » où chaque caractère peut être décomposé en 6 pseudo-pixels.

Par exemple, le caractère suivant, qui possède 4 pseudo-pixels blancs et 2 noirs, sera codé par le nombre (écrit en binaire) : 0b100111

 

 

Coté Arduino, nous utiliserons la bibliothèque Minitel1B_Hard.

#include <Minitel1B_Hard.h>

Une fois le Minitel relié aux ports de communication série Rx et Tx de l’Arduino, il faut créer l’objet représentant le Minitel :

Minitel minitel(Serial);

puis configurer la vitesse de communication :

minitel.changeSpeed(minitel.searchSpeed());

 

Ensuite, cette bibliothèque permet, entre autres fonctionnalités très intéressantes, de :

  • effacer l’écran : minitel.newScreen()
  • passer en mode graphique : minitel.graphicMode()
  • écrire un caractère de 6 pseudo-pixels à l’écran :
    • à la position du curseur : minitel.graphic(0b110110)
    • à la position souhaitée (ici x=30, y=15) minitel.graphic(0b110110, 30, 15)
Code source complet (version carte SD)
/*
  Programme d'affichage d'une image en noir&blanc sur un Minitel

  Lecture d'un fichier sur la carte SD
  Affichage via port série

  Voir : https://info.blaisepascal.fr/3615-tuveuxmaphoto
  
 */

#include <SPI.h>
#include <SD.h>

File imgFile;

#include <Minitel1B_Hard.h>
  
// Minitel minitel(Serial);  // Le premier port série matériel de l'ATMega 1284P (RXD0 TXD0), 
                             // ou celui de l'Arduino UNO
Minitel minitel(Serial1);  // Le deuxième port série matériel de l'ATMega 1284P (RXD1 TXD1).

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ;
  }

  // fonction bloquante ... ???
  //minitel.changeSpeed(minitel.searchSpeed());
  minitel.newScreen();
  minitel.graphicMode();
  
  Serial.println("init ...");
  if (!SD.begin(4)) {
    Serial.println("init failed!");
    while (1);
  }
  Serial.println("init done.");

  // Affichage de l'image sur le minitel
  //afficher_image("spock_nb");
  afficher_image_couleur("spock");
}

void loop() {
}


/*********************************************************
  Fonctions d'affichage sur le minitel
**********************************************************/
void afficher_image(String imgFileName){
  imgFile = SD.open(imgFileName);
  if (imgFile) {
    Serial.println(imgFileName);

    // Lecture du fichier image
    byte pixel[1];
    while (imgFile.available()) {
       imgFile.read(pixel,1);
       minitel.graphic(pixel[0]);
       }
    // Fermeture du fichier
    imgFile.close();
    
    } else {
    Serial.print("Erreur ouverture ");
    Serial.println(imgFileName);
  }
}

void afficher_image_couleur(String imgFileName){
  imgFile = SD.open(imgFileName);
  if (imgFile) {
    Serial.println(imgFileName);

    // Lecture du fichier image
    byte pixel[3];
    while (imgFile.available()) {
       imgFile.read(pixel,3);
       minitel.attributs(pixel[0]);
       minitel.attributs(pixel[1]);
       minitel.graphic(pixel[2]);
       }
    // Fermeture du fichier
    imgFile.close();
    
    } else {
    Serial.print("Erreur ouverture ");
    Serial.println(imgFileName);
  }
}
Code source complet (version WebServer)

Version POST

Bibliothèque Base64 utilisée : https://github.com/dojyorin/arduino_base64

/* Programme d'affichage d'une image en noir&blanc sur un Minitel 
 
 Image passée en paramètre d'une requête HTTP POST 
 via un formulaire
 
 Affichage via port série
 
 Voir : https://info.blaisepascal.fr/3615-tuveuxmaphoto
*/

#include <SPI.h>
#include <Ethernet.h>
#include <Minitel1B_Hard.h>
#include "arduino_base64.hpp"

//Minitel minitel(Serial);  // Le premier port série matériel de l'ATMega 1284P (RXD0 TXD0), 
                             // ou celui de l'Arduino UNO
Minitel minitel(Serial1);  // Le deuxième port série matériel de l'ATMega 1284P (RXD1 TXD1).

byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
EthernetServer server = EthernetServer(80);

byte buf[3842]; // données base64
int decodedLen = 0;
int b64Len = 0;
String readString = ""; 

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ;
  }
  Serial.println("Minitel Image WebServer");

  // fonction bloquante ... ???
  //minitel.changeSpeed(minitel.searchSpeed());
  minitel.newScreen();
  minitel.graphicMode();

  // start the Ethernet connection:
  Serial.println("Initialize Ethernet with DHCP:");
  if (Ethernet.begin(mac) == 0) {
    Serial.println("Failed to configure Ethernet using DHCP");
    if (Ethernet.hardwareStatus() == EthernetNoHardware) {
      Serial.println("Ethernet shield was not found");
    } else if (Ethernet.linkStatus() == LinkOFF) {
      Serial.println("Ethernet cable is not connected.");
    }
  }

  // start the server
  server.begin();
  Serial.print("Server is at ");
  Serial.println(Ethernet.localIP());
}


void loop() {
  // listen for incoming clients
  EthernetClient client = server.available();
  if (client) {
    //Serial.println("new client");
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    readString = ""; 
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        Serial.write(c);
        readString += c;

        if (c == '\n' && currentLineIsBlank) {
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println("Connection: close");
          client.println();
          client.println("<!DOCTYPE HTML>");
          client.println("<html>");
          client.println("<head><meta charset='UTF-8'/></head>");
          client.println("<form method='POST' id='img' enctype='text/plain'>");
          client.println("<textarea name='img' form='img' rows='45' cols='100'></textarea>");
          client.println("<input type='submit' value='Envoyer'>");
          client.println("</form>");

          if (readString.startsWith("POST")) {
            int lenPos = readString.indexOf("Length:")+8;
            int endPos = readString.indexOf("Origin:")-1;

            String l = readString.substring(lenPos, endPos);
            b64Len = l.toInt();
            //readString = "";
            
            for (int i = 0 ; i<b64Len ; i++){
              char cc = client.read();
              if (i>3) {
                buf[i-4] = cc;
              }
            }
            b64Len = b64Len-4;
            decodedLen = base64::decodeLength(buf);
            
            client.println("Image :</br>");
            client.print("<ul><li>Base64 : ");
            client.println(String(b64Len));
            client.print("</li><li>Décodée : ");
            client.println(String(decodedLen-1));
            client.println("</li></ul>");
          }
          
          client.println("</html>");
          break;
        }
        
        if (c == '\n') {
          currentLineIsBlank = true;
        } else if (c != '\r') {
          currentLineIsBlank = false;
        }
      }
    }
    // give the web browser time to receive the data
    delay(1);
    // close the connection:
    client.stop();
    readString = "";
    Serial.println("client disconnected");
    
    if (decodedLen > 0 && decodedLen < 3842) {
      afficher_image_couleur();
    }
    decodedLen = 0;
    b64Len = 0;
  }
}


void afficher_image_couleur(){
  Serial.println("Affichage");
  uint8_t decoded[decodedLen];  
  base64::decode(buf, decoded);

  minitel.newScreen();
  minitel.graphicMode();

  if (decodedLen > 1500) { // Couleur
    //Serial.println("Couleur");
    for (int i=0 ; i<decodedLen ; i=i+3) {
      minitel.attributs(decoded[i]);
      minitel.attributs(decoded[i+1]);
      minitel.graphic(decoded[i+2]);
    }
  } else {                // N&B
    //Serial.println("N&B");
    for (int i=0 ; i<decodedLen ; i++) {
      minitel.graphic(decoded[i]);
    }
  }
}

Version GET (ancien)

/* Programme d'affichage d'une image en noir&blanc sur un Minitel 
 
 Image passée en paramètre d'une requête HTTP GET encodée en Base64
 <IP>/?img=s48eh8s3z8fd1vf...
 
 Affichage via port série
 
 Voir : https://info.blaisepascal.fr/3615-tuveuxmaphoto

*/

#include <SPI.h>
#include <Ethernet.h>
#include <Minitel1B_Hard.h>
#include <gBase64.h>

// Minitel minitel(Serial);  // Le premier port série matériel de l'ATMega 1284P (RXD0 TXD0), 
                             // ou celui de l'Arduino UNO
Minitel minitel(Serial1);  // Le deuxième port série matériel de l'ATMega 1284P (RXD1 TXD1).

byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
IPAddress ip(192, 168, 0, 177);
EthernetServer server(80);

String readString = ""; 
String img = "";

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ;
  }
  Serial.println("Minitel Image WebServer");

  // fonction bloquante ... ???
  //minitel.changeSpeed(minitel.searchSpeed());
  minitel.newScreen();
  minitel.graphicMode();

  Ethernet.begin(mac, ip);

  // Check for Ethernet hardware present
  if (Ethernet.hardwareStatus() == EthernetNoHardware) {
    Serial.println("Ethernet shield was not found.  Sorry, can't run without hardware. :(");
    while (true) {
      delay(1); // do nothing, no point running without Ethernet hardware
    }
  }
  if (Ethernet.linkStatus() == LinkOFF) {
    Serial.println("Ethernet cable is not connected.");
  }

  // start the server
  server.begin();
  Serial.print("server is at ");
  Serial.println(Ethernet.localIP());
}


void loop() {
  // listen for incoming clients
  EthernetClient client = server.available();
  if (client) {
    Serial.println("new client");
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        Serial.write(c);
        readString += c;
        
        if (c == '\n' && currentLineIsBlank) {
          img = get_img(readString);
          // send a standard http response header
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println("Connection: close");
          client.println();
          client.println("<!DOCTYPE HTML>");
          client.println("<html>");
          client.println("Image Base64 :");
          client.println(img);
          client.println("</html>");
          break;
        }
        if (c == '\n') {
          currentLineIsBlank = true;
        } else if (c != '\r') {
          currentLineIsBlank = false;
        }
      }
    }
    // give the web browser time to receive the data
    delay(1);
    // close the connection:
    client.stop();
    Serial.println("client disconnected");

    afficher_image_couleur(img);
    readString="";
    img = "";
  }
}



String get_img(String readString) {
  int imgPos = readString.indexOf("img=")+4;
  int HTTPpos = readString.indexOf("HTTP")-1;
  String img = "";
  if(imgPos >0) {
    img = readString.substring(imgPos, HTTPpos);
    Serial.println(img);
  }
  return img;
}



void afficher_image_couleur(String imgString){
  byte buf[imgString.length()];
  imgString.getBytes(buf, imgString.length());
  
  int decodedLen = base64_dec_len(buf, imgString.length());
  char decoded[decodedLen];
  
  base64_decode(decoded, buf, imgString.length());

  //Serial.println(imgString.length());
  for (int i=0 ; i<decodedLen ; i=i+3) {
    minitel.attributs(decoded[i]);
    minitel.attributs(decoded[i+1]);
    minitel.graphic(decoded[i+2]);
    //Serial.println(decoded[i]);
    //Serial.println(decoded[i+1]);
    //Serial.println(decoded[i+2]);
  }
}

 

Configuration du Minitel
  • Sortie du répertoire: Fnct + Sommaire
  • Passage en mode périphérique: Fnct + T puis A
  • Désactivation de l’écho du terminal: Fnct + T puis E
  • Connexion
    • 9600 bauds : Fnct + P puis
    • 4800 bauds : Fnct+P puis 4
    • 1200 bauds : Fnct + P puis 1

 

 


L’objectif est de réaliser un programme Python qui permette de convertir n’importe quelle image numérique dans un format compatible avec le protocole d’affichage du Minitel.

Pour la phase de développement, nous utiliserons l’image suivante (cliquer pour télécharger) :

 


Image en noir et blanc

Pour manipuler les images, nous utiliserons la bibliothèque PIL, et plus particulièrement l’objet Image.

 

Ouverture de l’image

Écrire une fonction ouvrir_image(chemin_fichier) qui renvoie un objet PIL.Image à partir d’un chemin de fichier.

Cette fonction devra gérer les éventuels problèmes qui pourraient se poser :

  • Fichier introuvable : on testera l’existence du fichier à l’aide de la fonction os.path.isfile() ;
  • Fichier incompatible : on encadrera la fonction d’ouverture de fichier image par un try: ... except: ... .

Si l’image n’a pas pu être ouverte correctement, la fonction renvoie None.

 

 

Redimensionnement

Une fois l’objet Image créé, il faut en modifier les dimensions, car la résolution de l’écran du Minitel (en pseudo-pixels) est de 25×3 lignes pour 40×2 colonnes. (25 étant le nombre de lignes de caractères et 40 le nombres de caractères par ligne).

On définira les variables globales qui définissent les dimensions de l’écran (en caractères) :

# Dimensions écran Minitel
W, H = 40, 24

 

On se propose de coder 3 modes différents de redimensionnement :

  • mode 0 : l’image est simplement redimensionnée aux nouvelles dimensions, il peut donc y avoir une déformation ;

 

  • mode 1 : l’image est tronquée pour conserver le rapport largeur/hauteur d’origine ;

 

  • mode 2 : pour respecter le rapport largeur/hauteur sans tronquer l’image, du noir est ajouté autour.

 

 

 

Écrire une fonction redimensionner(img, mode = 0) qui renvoie un nouvel objet Image aux dimensions adaptées à l’écran du Minitel, en utilisant le mode donné en argument à la fonction.

 

Conversion en Noir et Blanc

La méthode .convert(mode) de l’objet Image renvoie un nouvel objet Image, convertit l’image selon différents modes, qui définissent la manière dont sont codés les pixels (profondeur, …). Voici un extrait de la documentation de PIL donnant quelques uns des modes disponibles :

  • "1" (1-bit pixels, black and white, stored with one pixel per byte)
  • "L" (8-bit pixels, black and white)
  • "RGB" (3×8-bit pixels, true color)
  • "RGBA" (4×8-bit pixels, true color with transparency mask)

 

Il existe donc un mode permettant de convertir directement en noir et blanc (2 niveaux !) mais cela ne permet pas de régler le seuil de luminosité délimitant les pixels noirs des pixels blanc.

Nous souhaitons pouvoir spécifier ce seuil afin d’optimiser l’apparence de l’image :

63 95 127 159 191

 

Écrire une fonction convertirNB(img, seuil = 127) qui renvoie une image convertie en 2 niveaux (noir et blanc) selon le seuil de luminosité spécifié (nombre entre 0 et 255).

 

Conversion au format Minitel

La trame d’octets envoyés au Minitel constituera l’ensemble des caractères (contenant chacun 6 pseudo-pixels) les uns à la suite des autres, en partant de la position x = 0 et y = 0 (en haut à gauche de l’écran).

Il va donc falloir construire chacun de ces caractères à partir des pixels de l’image.

Pour obtenir la valeur d’un pixel d’une image, on peut utiliser la méthode .getpixel((x, y)).

ATTENTION, en mode "1" (1 bit par pixel) .getpixel() renvoie soit 0, soit 255.

 

Écrire la fonction convertir_pixels_octet(img, x, y) qui renvoie l’octet construit à partir des valeur des 6 bits représentant les 6 pixels à proximité du pixel de coordonnées (x, y).

 

Écrire la fonction convertirMinitel(img) qui renvoie la liste d’octets constituant la trame à envoyer.

 

Envoi au minitel

Quel que soit le moyen choisi pour envoyer les données au Minitel, il faut commencer par convertir la liste d’entiers en tableau d’octet, un type à part entière appelé bytearray.

b = bytearray(m)

 

Version fichier

try:
    with open(fichier_trame, 'wb') as f:
        f.write(bytearray(m))
except:
    print("Impossible d'écrire dans le fichier %s !" %fichier_trame)
    sys.exit(1)

 

 

Version serveur Web

Pour la version Web, on commence par afficher la trame au format base64 (un format qui permet de convertir un tableau d’octets en une chaîne de caractères « imprimables »).

print(base64.b64encode(bytearray(m)))

Ensuite, à l’aide d’un « simple » copier/coller, on peut passer cette chaîne de caractères comme paramètres dans une URL (méthode GET) :

http://172.16.16.177/?img=KAAAAAAAABQA ... AAAAA

ou bien la coller dans la zone de texte du formulaire (méthode POST).

 

Résultat attendu

 

 


Image en niveaux de gris

Les caractères peuvent également être affichés en 8 couleurs différentes. Mais sur un écran en noir et blanc, cela rend des niveaux de gris.

La bibliothèque Arduino Minitel1B_Hard permet de spécifier les 2 couleurs pour chaque caractère : le caractère lui-même et son fond.

 

Pour cela, avant d’afficher un caractère, on peut modifier les attributs d’affichage à l’aide de la fonction minitel.attribut()

Voici les codes des couleurs, tirés du code source de la bibliothèque :

// Couleur de caractère
#define CARACTERE_NOIR    0x40
#define CARACTERE_ROUGE   0x41
#define CARACTERE_VERT    0x42
#define CARACTERE_JAUNE   0x43
#define CARACTERE_BLEU    0x44
#define CARACTERE_MAGENTA 0x45
#define CARACTERE_CYAN    0x46
#define CARACTERE_BLANC   0x47
// Couleur de fond
#define FOND_NOIR         0x50
#define FOND_ROUGE        0x51
#define FOND_VERT         0x52
#define FOND_JAUNE        0x53
#define FOND_BLEU         0x54
#define FOND_MAGENTA      0x55
#define FOND_CYAN         0x56
#define FOND_BLANC        0x57

 

Par exemple, pour modifier la couleur du caractère, on exécutera :

minitel.attribut(CARACTERE_JAUNE);

 

Écrire une fonction convertir8(img) qui renvoie une image convertie en 8 niveaux de gris.
Chaque caractère ne pouvant contenir que 2 couleurs, il faut trouver une méthode pour convertir chaque groupe de 6 pixels à 8 niveaux de gris en 6 pixels à 2 niveaux de gris, de sorte que les motifs soient visuellement les plus proches possible :
Écrire une fonction convertirMinitel8(img) qui renvoie la liste d’octets constituant la trame à envoyer.
Chaque caractère sera codé par 3 octets consécutifs : la couleur de fond, la couleur du caractère, le code du caractère semi-graphique.
Attention : l’ordre des couleurs ne correspond pas avec celui des niveaux de gris ! Si on ne modifie pas cet ordre on risque d’obtenir ceci :

Voici ce qu’on voit lorsque l’on affiche les couleurs dans l’ordre de leur numéro de code :

 

Résultat attendu

Vous aimerez aussi...

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *