Une boîte à histoire OpenSource - Partie 2

  • By Anthony Rabine
  • 05 Oct, 2020
Une boîte à histoire OpenSource - Partie 2

La boîte à histoires Lunii, création française, est un objet électronique épatant pour les enfants : pas d’écran (quelques images seulement), une molette et un haut parleur suffisent pour écouter des histoires à créer. Les gosses sont accrocs ! Ce second chapitre va s’intéresser à l’analyse des fichiers contenant les histoires et aux choix matériels.

Planning super précis

Essayons un peu d’organiser un peu cela dans le temps. Sachant que c’est un projet personnel réalisé sur mon temps libre, je me fixe des jalons trèèèèès flexibles.

  1. La première étape sera la réalisation d’un logiciel sur PC permettant de lire les packs.
  2. Ensuite viendra la partie embarquée avec les étapes classiques mais néanmoins obligatoires :
  • Choix des cartes de développement, par fonction
  • Test des cartes
  • Utilisation de Plans B si besoin

image

On choisit la carte processeur

On l’a vu, l’architecture logicielle sera facilement portable. Le but étant de créer une boîte à histoire facilement réparable, il faut faciliter l’achat de matériel de remplacement. Mais bon, pour ce premier prototype, il faudra bien se fixer une première cible.

L’architecture électronique est assez classique dans l’embarqué, rien de fou fou, un microcontrôleur unique centralise tous les périphériques. Il y aura néanmoins quelques aspects auxquels il faudra faire attention :

  • Le microcontrôleur doit disposer d’un périphérique I2S pour la sortie audio
  • L’USB doit servir pour la charge mais aussi pour transférer les histoires (ou alors, prévoir un autre moyen, par wifi peut-être ?) : il faut donc un USB natif et non via une passerelle
  • Je veux pouvoir déboguer (mettre des points d’arrêt) à l’aide d’une sonde Segger
  • Plutôt une un coeur ARM que je connais bien

Comment choisir une carte de développement ? Les sources sont grandes ! Voici une liste largement pas exhaustive pour vous procurer des cartes :

  • Arduino : la fondation propose PLEIN de cartes différentes génériques ou spécialisées en plus de cartes hat/header/extensions
  • SeedStudio
  • AdaFruit
  • Teensy
  • Olimex
  • Micro:Bit
  • Sparkfun
  • Raspberry PI
  • Les cartes de développement des fondeurs : ST / Microchip / TI …

La liste est longue et j’en oublie…

J’ai passé beaucoup de temps sur le choix de ma carte car je souhaitais qu’elle remplisse plusieurs rôles supplémentaires. Une des fonctions que je connais le moins est tout ce qui se rapporte à la charge des batteries. Il me faudrait donc une carte qui embarque cette fonction, si possible ! Ce n’est pas un critère fondamental car je peux utiliser une carte externe pour cela. On va dire que c’est le bonus !

Voici donc mon choix : c’est une carte dédiée à l’audio, l’Arduino MKR Zero qui remplit tout mon cahier des charges, c’était inespéré.

image

Nous voici donc avec un processeur SAMD21 Cortex-M0+, USB natif, I2S + SPI + UART comme notre diagramme le veux, doté de 256Ko de flash et 32Ko de RAM ; une zone de 8Ko est dédiée au bootloader ce qui nous permettra de recharger le firmware (ce n’est pas à l’ordre du jour pour notre prototype). La carte dispose d’un lecteur SD-Card, donc parfait !

Voici le brochage :

image

Enfin, cerise sur le gâteau mais obligatoire pour moi : la possibilité de connecteur une sonde de débogage via une empreinte sous la carte :

image

Parfait, sur le papier du moins. Voici la fonction que remplit la carte Arduino par rapport à notre architecture. Dans les prochains articles, nous nous pencherons sur les autres périphériques.

On commence l’analyse des packs

Le logiciel LuniiStore télécharge les packs d’histoire en local sur le disque avant de les envoyer vers la boîte à histoire. Sous Linux, ils sont situés dans ~/.local/share/Luniitheque/packs/ (sous Windows : %UserProfile%\AppData\Roaming\Luniitheque) et possèdent une extension .pk. Voici par exemple un dossier contenant une dizaine de packs. Chaque pack est une archive Zip contenant les sons, les images et les indications pour les transitions.

image

Dézippons un pack. Voici une indication sur l’utilité de chaque fichier et répertoire, que je retrace ici grâce au logiciel STUdio disponible sur Github (https://github.com/marian-m12l/studio).

image

  • Node Index (fichier ‘ni’) : il n’est pas chiffré, tout est en clair, c’est le script de déroulement de l’histoire
  • List Index (fichier ‘li’) : les 512 premiers octets sont chiffrés
  • Resource Index (fichier ‘ri’) : les 512 premiers octets sont chiffrés
  • Sound Index (fichier ‘si’) : les 512 premiers octets sont chiffrés

À l’aide du logiciel STUdio et de son auteur, j’ai compris certaines choses sur la structure des données. Le support du nouveau format de fichier est encore à l’état non fonctionnel dans le logiciel et certaines parties des fichiers sont chiffrés et non interprétables tels quels au moment de l’écriture de cet article.

Donc, le résultat présenté ici n’est pas définitif et sera amélioré au fil du temps !

Commençons notre analyse par les fichiers images pour voir si l’on parvient à les afficher. Chaque fichier possède une zone de 512 octets complètement obscur pour le moment. La seconde zone est une image au format BMP, comme pour la première version du logiciel. Mais ce sont les données brutes des pixels uniquement, l’en-tête du format BMP est dans la première zone obscure. Nous connaissons certaines paramètres comme la résolution (320x240) mais aussi le type d’encodage du BMP (4-bits par pixel), ce qui donne une image en niveaux de gris. Le problème est qu’il nous manque une information : celle de la palette de couleurs, qui est située dans l’en-tête. Bon nous verrons ce que nous pourrons faire.

image

Démarrage du logiciel

Nous allons commencer notre logiciel PC de lecture des packs. Il sera réalisé en Qt, et toutes les routines utiles seront placées dans des fichiers séparés por être utilisés par le firmware plus tard.

Le logiciel est équipé une librairie Zip permettant d’ouvrir en mémoire l’archive et de charger n’importe quel fichier.

Une fois ceci fait, nous décodons le Bitmap à partir de l’adresse 0x200. La routine principale est la suivante :

uint32_t pixel = 0; // specify the pixel offset
uint32_t i = 0;
do
{
    uint8_t rleCmd = compressed[i];
    if (rleCmd > 0)
    {
        uint8_t val = compressed[i + 1];
        // repeat number of pixels
        for (uint32_t j = 0; j < rleCmd; j++)
        {
            uint32_t byte_index = pixel / 2;
            if ((pixel & 1) == 0)
            {
                decompressed[byte_index] = (val & 0xF0);
                pixel++;
            }
            else
            {
                decompressed[byte_index] |= (val & 0x0F);
                pixel++;
            }
        }
        i += 2; // jump pair instruction
    }
    else
    {
        uint8_t second = compressed[i + 1];
        if (second == 0)
        {
            if (pixel % width)
            {
                // end of line
                uint32_t lines = pixel / width;
                uint32_t remaining = width - (pixel - (lines * width));

                pixel += remaining;
            }
            i += 2;
        }
        else if (second == 1)
        {
            std::cout << "End of bitmap" << std::endl;
            i += 2;
            goto end;
        }
        else if (second == 2)
        {
            // delta N pixels and M lines
            pixel += compressed[i + 2] + compressed[i + 3] * width;
            i += 4;
        }
        else
        {
            // absolute mode
            uint8_t *ptr = &compressed[i + 2];
            // repeat number of pixels
            for (uint32_t j = 0; j < second; j++)
            {
                uint32_t byte_index = pixel / 2;
                if ((pixel & 1) == 0)
                {
                    decompressed[byte_index] = (*ptr & 0xF0);
                    pixel++;
                }
                else
                {
                    decompressed[byte_index] |= (*ptr & 0x0F);
                    ptr++;
                    pixel++;
                }
            }
            i += 2 + (second / 2);

            // padded in word boundary, jump if necessary
            if ((second / 2) % 2)
            {
                i++;
            }
        }
    }

    if (pixel >= (width * height))
    {
        std::cout << "Error!" << std::endl;
    }
}
while( i < compressedSize);

Le principe de la compression se base sur la répétition des pixels de même couleur. Le codage exact est disponible sur le site de Microsoft (“Bitmaps with 4 Bits per Pixel”) .

Le résultat est un buffer décompressé. On invente notre propre palette en créant 16 niveaux de gris et on affiche le résultat :

image

Pas si mal !

Lancement du projet OpenStoryBox

Je profite de cet article pour lancer le projet d’alternative libre à la boîte à histoire : l’Open Story Box. Deux sites permettent de suivre le projet :