Codeur JUNIOR confiné - Episode 4, Application connectée : les textos (partie 2)

Codeur JUNIOR confiné - Episode 4, Application connectée : les textos (partie 2)

Suite de notre troisième projet familial de code. Pour rappel, on veut faire communiquer deux puces micro:bit, pour s'envoyer des messages - des textos par radio !

Aucun texte alternatif pour cette image

La partie la plus complexe a été de concevoir une interface de saisie de messages texte, avec deux boutons et un gyroscope : #dymoAnnées80 😁 (voir l'épisode 3 ici).

On peut donc attaquer l'implémentation de notre outil, via notre diagramme d'architecture, ci-contre.

Au départ, un des deux appareils entre en mode envoi via le bouton A, et l'autre en mode réception via le bouton B.

La saisie se fait au gyroscope et aux boutons, lettre par lettre, comme vu précédemment. Une fois le message terminé, une bonne secousse fait passer l'appareil en mode relecture, pour pouvoir relire son message une dernière fois, et de là au choix l'envoyer (A), le réécrire (B) ou reprendre la saisie là où on en était (A+B).

L'envoi fait passer l'appareil en mode réception, automatiquement. Pendant ce temps, l'appareil qui reçoit un message l'affiche, et permet ensuite au choix de l'afficher en boucle (B) ou de passer en mode saisie/envoi à son tour (A).

Etat de départ

Aucun texte alternatif pour cette image

Pour bien démarrer, on a commencé par définir nos actions d'initialisation.

  1. Définir les variables
  2. Demander à l'utilisateur le mode qu'il choisit
  3. Par la suite, on s'est aussi rendu compte que définir un groupe radio était une bonne idée.

Le groupe radio expliqué aux enfants. "Imagine qu'on a 10 tuyaux de couleurs différentes. Je parle dans le tuyau rouge et que tu écoutes de l'autre côté du tuyau rouge, tu m'entends ? oui. Ok, maintenant imagine que je parle dans le tuyau rouge et que tu écoutes de l'autre côté du tuyau bleu, tu m'entends cette fois ? non.". Là c'est pareil, on se met d'accord sur une couleur de tuyau.

Note: après lecture de la spec du micro:bit, le code du programme est utilisé pour obtenir un numéro de groupe radio par défaut. En pratique donc, même sans définir un groupe, deux micro:bits qui font tourner le même code pourront communiquer sans souci. Mais assigner un groupe fixe reste plus propre - par exemple pour des montées de versions non synchrones entre les appareils.

Programmation fonctionnelle

On a déjà eu l'occasion avec les enfants de définir ce qu'étaient les fonctions. Là, on en a limite abusé, mais c'est plus efficace à maintenir et ça évite le code mal factorisé...

Et surtout, ça permet une approche plus efficace et un code plus lisible :

  1. Quelles sont les différentes actions que je veux implémenter ? (et j'en fais des fonctions)
  2. Quel est le déroulé de mon programme (l'enchainement événement / action) ?

On va donc dans un premier temps se focaliser sur toutes les fonctions nécessaire à notre programme.

Quelle est la lettre sélectionnée ?

Aucun texte alternatif pour cette image

Cette méthode est issue de notre POC précédent. En un mot : s'assurer d'une part que notre "position dans l'alphabet" est toujours comprise entre 0 et 25; et d'autre part renvoyer la lettre correspondante dans l'alphabet.

Note: il semble que la fonction sous-chaine intègre un calcul de modulo automatiquement. Mais bon quoi qu'il arrive c'est plus propre de le rendre explicite !

Changer la position : mon premier setter avec effet de bord

Aucun texte alternatif pour cette image

Il y a plusieurs manières dans notre diagramme d'architecture de modifier la lettre courante. Démarrer un message, reprendre un message, pencher l'appareil, ...

Et à chaque fois que la position change (et la lettre courante aussi donc), on aimerait tant qu'à faire afficher la nouvelle lettre, vérifier qu'on est dans le bon intervalle [0,25], ...

Et pour éviter de le faire à chaque fois, et être sur de ne rien oublier : on a décidé de ne pas toucher directement à la variable, sauf en passant par une fonction qui le fait pour nous ! C'est ça notre setter.

En pratique, les limites de l'intervalle sont validées par l'affichage, cf. plus haut.

Décaler la position d'une valeur donnée

Aucun texte alternatif pour cette image

La fonction suivante s'appuie, une fois de plus, sur la précédente. Vu que pour beaucoup, nos changements de position vont se faire à coup de décalages de 1 dans un sens, ou dans l'autre, autant faire une fonction rapide pour ça !

Ecrire / effacer

Aucun texte alternatif pour cette image

Notre message sera stocké dans la variable éponyme. Pour écrire dedans, on va aussi faire des fonctions, c'est plus pratique ensuite pour relire le code fonctionnel.

Une première méthode va ajouter la lettre sélectionnée au message en cours, une autre va supprimer la dernière lettre du message en cours (via un message = substring(message) avec une lettre en moins).

Note: on a pu valider aussi que sous-chaine de taille -1 d'une chaine vide ne pose pas de souci.

Démarrer l'étape d'écriture

Aucun texte alternatif pour cette image

Démarrer l'étape d'écriture, c'est réinitialiser notre message, définir l'état courant à "ECRIRE" puisque c'est notre étape en cours, redémarrer l'alphabet à la lettre A...

Autant d'étapes qu'on va regrouper dans une fonction.

En pratique, on s'est rendu compte que la "reprise du message", qui permet de le modifier après l'avoir relu, et avant de l'avoir envoyé, a elle même besoin de passer dans un état ECRIRE. Plutôt que de faire deux fonctions, on a utilisé un paramètre pour ça (le début du message : parfois une chaine vide, parfois un message à reprendre)

Note: il aurait été plus propre de faire un booléen conserver_le_message mais on s'est mis à imaginer des choses comme reprendre un message reçu pour le renvoyer, ... bref plein de choses qu'on n'a pas faites au final mais le paramètre message semblait mieux sur le coup.

L'étape de relecture

Aucun texte alternatif pour cette image

La relecture, ça n'est rien de plus qu'un changement d'état, avec un affichage du message.

Cependant, on aimerait aussi suspendre toutes les actions possibles durant l'affichage du message. On va donc procéder ainsi : on commence par mettre un état blanc (on n'écoute donc personne), on affiche le message, et enfin on le relit.

L’émission

Aucun texte alternatif pour cette image

Une fois notre message prêt à partir, il faut le transmettre. Pareil que précédemment, on va mettre un état blanc, pour éviter d'interférer pendant l'envoi. Et une fois l'envoi terminé, on bascule sur la réception.

Alors oui, pour faire propre, il aurait fallu mettre le démarrage de la réception en dehors de la fonction "Envoyer message" : en l'état, on ne peut pas faire un envoi sans basculer automatiquement en réception. Mais ça nous va : c'était la spec.

Par contre, le bloc pour démarrer la réception a absolument besoin quant à lui d'être isolé, puisqu'il est possible de démarrer une réception sans avoir émis avant (c'est à dire, quand on démarre le programme en tant que récepteur).

Dernières fonctions : gestion de la réception de données

Aucun texte alternatif pour cette image

Il nous reste à savoir ce qu'on fait avec un message reçu : tout d'abord on le stocke et ensuite on l'affiche (et on change l'état bien entendu).

L'affichage se fait aussi dans une fonction à part. Ca parait bizarre et/ou inutile, mais en fait non : c'est pour pouvoir ré-afficher le message encore et encore, sans l'avoir nécessairement reçu à chaque fois. Une fonction "replay" quoi.

Le reste du code : les enchaînements "événements / fonctions"

Ne reste plus qu'à définir ce qu'on fait quand tel ou tel événement intervient. C'est très simple et très lisible, grâce à toutes nos fonctions.

Quand j'appuie sur A

L'action a faire dépend de l'étape en cours (la valeur de la variable "Etat"). On se rapportera au diagramme au tout début de l'article pour savoir quoi faire à chaque fois.

Aucun texte alternatif pour cette image
  • Si c'est le début, A = démarrer une saisie
  • Si c'est une saisie, A = ajouter une lettre
  • Si c'est une relecture, A = envoyer le message
  • Si c'est un affichage de message reçu, A = démarrer un nouveau message

L'avantage de la programmation fonctionnelle est visuel, les enfants n'ont qu'à lire ce qui est écrit noir sur blanc (enfin, blanc sur coloré parfois) pour comprendre ce que fait ce bouton A !

Quand j'appuie sur B

Tout aussi fourni, le bouton B :

Aucun texte alternatif pour cette image
  • Si c'est le début, B = passer en mode réception
  • Si c'est une saisie, B = supprimer la dernière lettre du message
  • Si c'est une relecture, B = recommencer le message à zéro
  • Si c'est une réception, B = afficher le message reçu à nouveau

Quand j'appuie sur A+B

Aucun texte alternatif pour cette image
  • Si c'est une saisie, A+B = ajout d'un espace au message.
  • Si c'est une relecture, A+B = reprendre l'écriture à la suite.


Note: au sujet de l'ajout de l'espace .... c'est pas bien, on a oublié une fonction! Mais bon, ça sera "ma première dette technique" 😁

Quand je penche à gauche ou à droite

Aucun texte alternatif pour cette image

Tout d'abord, ces deux actions n'ont d'impact que dans le cas où nous sommes en train d'écrire le message, sinon on les ignore.

Et de plus, notre prototype de saisie (voir l'épisode 3 ici) nous avait permis de constater que l'événement "incliné" ne se produit que lorsqu'on passe de l'état "à plat" à l'état "incliné", pas quand la puce est déjà inclinée.

La boucle à l'intérieur nous permettait de faire défiler l'alphabet sans avoir besoin de redresser la puce à chaque fois : l'événement déclenche la boucle, qui se répète tant que l'inclinaison est effectivement constatée.

Quand je secoue la puce

Aucun texte alternatif pour cette image

Secouer c'était un peu notre action de secours, puisque A, B, A+B et pencher étaient déjà pris lors de l'étape de saisie.

Secouer nous permet de passer du mode Saisie au mode Relecture, juste avant d'envoyer.

Quand je reçois des données

Aucun texte alternatif pour cette image

Reste une dernière étape, et pas des moindres : que faire quand on reçoit des données (c'est à dire un message) ? Tout simplement : enregistrer le message reçu (et passer en mode Affichage, cf nos fonctions plus haut)

L'alpha testing

C'est tout ? oui c'est tout. L'application fonctionne, et même bien. Le moment est venu de s'amuser à s'envoyer des messages à tout va : c'est la phase d'alpha testing (ou de beta testing, le produit étant déjà bien stable, mais bon).

Les feedbacks

Ceci étant dit, après plusieurs échanges, on s'est rendu compte que ça marche, oui, c'est fun, ok, mais il y a quelques soucis quand même...

Le bug du "moi moi moi"

Comme on l'a vu, au démarrage, celui qui envoie appuie sur A, celui qui écoute appuie sur B. Sauf que si les deux veulent envoyer, c'est un peu la grouille...

Et en pratique, sur de vrais systèmes de communication, un utilisateur ne peut pas crier à l'autre "non c'est à mon tour de commencer !"

C'est lent...

Oui, c'est particulièrement lent. La saisie n'est pas hyper rapide, mais ce n'est pas ça le pire.

Tous les messages statiques ("Envoi du message", "Message reçu !", le message de démarrage), bref tous ces messages mettent beaucoup de temps à s'afficher alors qu'en pratique on les connait, ils ne changent pas !

Le test de debug

On aimerait bien faire tourner notre code dans l'IDE pour vérifier que ça marche sans avoir besoin de télécharger le binaire, l'envoyer sur chaque puce, ...

Ca marche pas des masses, et une petite astuce ne serait pas de trop.

La V2

On a donc planché sur une V2, histoire de corriger ces différents soucis.

Qui commence ?

Un premier patch : savoir qui commence. A vrai dire, on ne sait pas, et on s'en fiche un peu, l'essentiel étant que les deux ne démarrent pas en même temps...

On a donc profité de la communication radio (la même que celle qui nous sert pour les messages eux-mêmes ! ), celui qui décide en premier de son rôle le notifie à l'autre avec un petit "J'envoie !" et ou un petit "J'écoute !"

Aucun texte alternatif pour cette image

Tous ces messages n'ont bien entendu de sens que si on est capable de les écouter / détecter / interpréter, mais grâce à notre armée de fonctions, c'est plutôt facile 😁

Aucun texte alternatif pour cette image

Si l'autre me dit qu'il envoie, j'active la réception. Et inversement je vais saisir un message si l'autre m'annonce qu'il va écouter.

Et un problème de réglé !

La lenteur

L'affichage de LED ne prend pas trop de temps en soi. C'est le défilement de texte qui est particulièrement lent.

Question : comment faire parvenir la même information, sans faire défiler de texte ???

Réponse : 🤔🙇‍♂️💡‼️ Avec des dessins 😁

Aucun texte alternatif pour cette image

On commence par se faire des fonctions, pour afficher deux icônes ✉️ et 🔔. On les utilise ensuite à l'envi.

Note : oui c'est pas très bien dessiné. Mais on a un écran avec une résolution de 5 x 5 je voudrais vous y voir !

Aucun texte alternatif pour cette image

Et voilà. V2 fonctionnelle et opérationnelle 😁

Note à propos du debug dans l'IDE : l'action supplémentaire "envoyer le nombre 0 par radio" dans la partie démarrage qu'on peut voir ci-dessus a été ajouté justement pour pallier le souci de debug. L'interface ne fait intervenir une deuxième puce que lorsqu'une émission est effectivement constatée - du coup on envoie n'importe quoi, c'est juste pour avoir la seconde puce à l'écran mais en pratique sinon oui ça sert à rien...

Aucun texte alternatif pour cette image

Le code !

Si le coeur vous en dit, vous pouvez toujours récupérer le même code en version JS, ci-dessous. Si vous avez une paire de micro:bits, ça peut vous intéresser 😁

function Démarrer_la_reception () {
    basic.clearScreen()
    Etat = "RECEPTION"
}
function Afficher_icone_enveloppe () {
    basic.showLeds(`
        # # # # #
        # # . # #
        # . # . #
        # . . . #
        # # # # #
        `)
}
function Afficher_message_reçu () {
    basic.showString(Message_reçu)
}
function Enregistrer_message_reçu (message_reçu: string) {
    Etat = "AFFICHAGE"
    Afficher_icone_sonnette()
    Message_reçu = message_reçu
    Afficher_message_reçu()
}
input.onButtonPressed(Button.A, function () {
    if (Etat == "DEBUT") {
        radio.sendString("J'envoie !")
        Démarrer_lécriture_dun_message("")
    } else if (Etat == "ECRIRE") {
        Ajouter_la_lettre_séléctionnée()
    } else if (Etat == "RELIRE") {
        Envoyer_message()
    } else if (Etat == "AFFICHAGE") {
        Démarrer_lécriture_dun_message("")
    }
})
function Envoyer_message () {
    Etat = ""
    Afficher_icone_enveloppe()
    radio.sendString(Message)
    Démarrer_la_reception()
}
function Changer_la_lettre_courante (position: number) {
    Position_dans_lalphabet = position
    basic.showString("" + (Quelle_est_la_lettre_séléctionnée_()))
}
input.onGesture(Gesture.TiltRight, function () {
    if (Etat == "ECRIRE") {
        while (input.isGesture(Gesture.TiltRight)) {
            Décaler_dans_lalphabet(1)
        }
    }
})
input.onGesture(Gesture.TiltLeft, function () {
    if (Etat == "ECRIRE") {
        while (input.isGesture(Gesture.TiltLeft)) {
            Décaler_dans_lalphabet(-1)
        }
    }
})
input.onButtonPressed(Button.AB, function () {
    if (Etat == "ECRIRE") {
        Message = "" + Message + " "
    } else if (Etat == "RELIRE") {
        Démarrer_lécriture_dun_message(Message)
    }
})
function Quelle_est_la_lettre_séléctionnée_ () {
    if (Position_dans_lalphabet < 0) {
        Position_dans_lalphabet += 26
    } else if (Position_dans_lalphabet >= 26) {
        Position_dans_lalphabet += -26
    }
    return Alphabet.substr(Position_dans_lalphabet, 1)
}
function Décaler_dans_lalphabet (décalage: number) {
    Changer_la_lettre_courante(Position_dans_lalphabet + décalage)
}
radio.onReceivedString(function (receivedString) {
    if (Etat == "DEBUT" && receivedString == "J'envoie !") {
        Démarrer_la_reception()
    } else if (Etat == "DEBUT" && receivedString == "J'écoute !") {
        Démarrer_lécriture_dun_message("")
    } else if (Etat == "RECEPTION") {
        Enregistrer_message_reçu(receivedString)
    }
})
input.onButtonPressed(Button.B, function () {
    if (Etat == "DEBUT") {
        radio.sendString("J'écoute !")
        Démarrer_la_reception()
    } else if (Etat == "ECRIRE") {
        Supprimer_la_dernière_lettre()
    } else if (Etat == "RELIRE") {
        Démarrer_lécriture_dun_message("")
    } else if (Etat == "AFFICHAGE") {
        Afficher_message_reçu()
    }
})
input.onGesture(Gesture.Shake, function () {
    if (Etat == "ECRIRE") {
        Démarrer_la_relecture()
    }
})
function Démarrer_lécriture_dun_message (début_du_message: string) {
    Etat = "ECRIRE"
    Message = début_du_message
    Changer_la_lettre_courante(0)
}
function Supprimer_la_dernière_lettre () {
    if (!(Message.isEmpty())) {
        Message = Message.substr(0, Message.length - 1)
    }
}
function Démarrer_la_relecture () {
    Etat = ""
    basic.showString(Message)
    Etat = "RELIRE"
}
function Afficher_icone_sonnette () {
    basic.showLeds(`
        . . # . .
        . # # # .
        . # # # .
        # # # # #
        . . # . .
        `)
}
function Ajouter_la_lettre_séléctionnée () {
    Message = "" + Message + Quelle_est_la_lettre_séléctionnée_()
}
let Message_reçu = ""
let Etat = ""
let Message = ""
let Position_dans_lalphabet = 0
let Alphabet = ""
radio.setGroup(1)
radio.sendNumber(0)
Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Position_dans_lalphabet = 0
Message = ""
basic.showLeds(`
    . # . . .
    # . # . .
    # # # . #
    # . # . .
    # . # . #
    `)
Afficher_icone_enveloppe()
basic.showLeds(`
    # # . . .
    # . # . .
    # # . . #
    # . # . .
    # # . . #
    `)
Afficher_icone_sonnette()
Etat = "DEBUT"


Identifiez-vous pour afficher ou ajouter un commentaire

Autres pages consultées

Explorer les sujets