IMA4 2018/2019 P2

De Wiki de Projets IMA


NumWorks et robot

Présentation générale

Description

Le projet consiste à commander un robot classique par une calculatrice NumWorks avec un programme Python. Après avoir conçu un robot classique à base d'Arduino, moteurs, sonar ultrason et suiveurs de lignes, il faudra modifier le logiciel d'une calculatrice NumWorks afin que cette dernière soit capable de communiquer avec le robot. Une fois la communication établie, nous développerons une API afin de contrôler le robot. Finalement, nous écrirons un programme python utilisant l'API pour transformer le robot en un robot suiveur de ligne avec arrêt en cas d'obstacle et redémarrage automatique lors de la disparition de l'obstacle.


L’intérêt de ce projet serait de permettre l'initiation à la robotique aux lycéens à moindre coût. Les lycéens ayant tous à acheter eux-mêmes une calculatrice, en leur imposant la Numworks, il n'y aurait que les robots à acheter pour un peu moins de 50 euros. Ils pourraient alors découvrir le monde de la programmation robotique en utilisant l'API développée sur la Numworks qui permet la programmation Python.

Objectifs

  • Conception d'un robot classique
  • Modification du logiciel d'une calculatrice NumWorks pour établir une communication entre le robot et la calculatrice
  • Développer une API permettant le contrôle du robot
  • Développer un programme Python transformant le robot en un robot suiveur de ligne avec arrêt en cas d'obstacle et redémarrage automatique lors de la disparition de l'obstacle

Préparation du projet

Cahier des charges

Robot :

  • Concevoir un robot mobile à partir d'un Arduino Uno, capable de détecter des obstacles grâce à un capteur de distance à ultrason et de suivre une ligne grâce à des capteurs suiveurs de ligne

Communication :

  • Établir une communication entre le robot et la calculatrice NumWorks en tentant de passer le micro-contrôleur en hôte USB et en utilisant le FTDI de l'Arduino

API :

  • Développer une API permettant de contrôler le robot :
    • fonctions pour contrôler les moteurs
    • fonctions pour lire la distance du sonar
    • fonctions pour lire l'information des suiveurs de ligne

Programme Python :

  • Développer un programme Python se servant de l'API pour transformer le robot en un suiveur de ligne. Il doit permettre au robot de s'arrêter en cas d'obstacle et redémarrer lorsque l'obstacle disparaît

Choix techniques : matériel et logiciel

- Robot :

- Calculatrice NumWorks

- Shield pour Arduino : Réalisation sur Eagle

- Communication : Modification de l'OS Numworks en C++ / C

- API et programme suivi de ligne : Développés en Python


Liste des tâches à effectuer

  • Assembler le robot
  • Réaliser un shield pour l'Arduino
  • Modifier le logiciel de la calculatrice NumWorks pour établir la communication
  • Développer l'API
  • Développer le programme Python


Réalisation du Projet

Feuille d'heures

Tâche Prélude Heures S1 Heures S2 Heures S3 Heures S4 Heures S5 Heures S6 Heures S7 Heures S8 Heures S9 Heures S10 Heures S11 Heures S12 Heures S13 Total
Analyse du projet 5H 5H+
Recherches / documentation 12H 6H 18H+
Conception du robot 30min 1H 3H 4H30
Réalisation du shield 16H30 2H30 7H 2H 2H 8H 4H 42H
Établissement de la communication 3H 6H 6H 9H 3H 8H 3H30 1H30 40H
Développement de l'API 11H 15H 10H 4H 3H 43H
Développement du programme Python 2H 24H 26H

Prologue

Semaine 1

  • Acquisition du matériel :

Le matériel nécessaire à la conception du robot était déjà disponible. Récupération d'un kit à assembler pour construire un robot à deux roues, d'un Arduino Uno, d'un sonar ultrason, d'un contrôleur de moteur ainsi que de trois suiveurs de ligne.

  • Installation du SDK NumWorks :

Ayant à modifier le logiciel de la calculatrice NumWorks afin de transformer la calculatrice en un périphérique USB hôte pour pouvoir contrôler le robot, j'ai installé le SDK fourni par l'éditeur qui offre un simulateur.

On récupère dans un premier temps le code :

  git clone https://github.com/numworks/epsilon.git


Avant de pouvoir lancer le simulateur, il faut installer de nombreuses dépendances :

  apt-get install bison build-essential dfu-util flex gcc-arm-none-eabi git libfltk1.3-dev libfreetype6-dev libpng12-dev

Une fois les dépendances installées, on peut compiler :

  make PLATFORM=simulator clean
  make PLATFORM=simulator

Puis on lance le simulateur par la commande :

  ./epsilon.elf
Simulateur NumWorks


  • Recherches / Documentation  :

L'USB On-The-Go (OTG) est une spécification qui permet à un périphérique USB de se comporter comme un hôte ou un esclave selon la situation. Par exemple, cela permet à une tablette (qui est de base un périphérique esclave) de pouvoir communiquer avec un autre périphérique USB tel qu'une souris, un clavier en agissant comme périphérique hôte puis d'être esclave lorsque connecté à un ordinateur. Cette spécification OTG introduit un 5ème pin, le pin "ID". Lorsque ce dernier est connecté à la masse, le périphérique est défini comme hôte par défaut. Si le pin n'est pas connecté, le périphérique est esclave par défaut.

Le port USB de la calculatrice NumWorks n'étant censé être utilisé que pour recharger la calculatrice ou la mettre à jour, la calculatrice est un périphérique esclave. Cela est confirmé par le Schematics de la calculatrice sur lequel on voit que le pin ID de l'USB n'est pas connecté :


Schematics du port USB de la NumWorks


Le micro-contrôleur STM32F412VGT6 utilisé est cependant compatible avec l'OTG d'après sa Datasheet et l'acronyme "OTG" apparaît à de nombreuses reprises dans le code du logiciel de la calculatrice NumWorks. Il faudra donc creuser dans ce sens pour faire de la calculatrice un hôte USB.

Semaine 2

  • Recherches / Documentation  :

Recherche et lecture de documentation pouvant aider à l'établissement de la communication entre la calculatrice et le robot. On retiendra trois documents qui pourront nous aider:

- Manuel d'utilisation de la librairie USB host du logiciel de développement des STM32 STM32Cube

- Manuel d'utilisation de la librairie USB On-The-Go des STM32F

- Manuel de référence du STM32F412


  • Établissement de la communication entre la calculatrice et le robot :

Début de modification du logiciel de la calculatrice NumWorks afin d'en faire un hôte USB pour pouvoir contrôler le robot. Pour cela, le manuel de référence du STM32F412 est une aide précieuse.

Dans un premier temps, nous modifions les différents registres d'initialisation de l'USB OTG du micro-contrôleur. Cela se passe dans la fonction initOTG() du fichier /ion/src/device/usb.cpp. | | | Dans ce fichier, on s'aperçoit d'une ligne qui force l'USB à agir en tant qu'esclave :

  OTG.GUSBCFG()->setFDMOD(true);

Pour forcer la calculatrice à apparaître comme un hôte USB, on modifie la ligne de la façon suivante :

  OTG.GUSBCFG()->setFHMOD(true);

Le manuel de référence du STM32F412 nous propose une démarche pour initialiser l'USB de la calculatrice en tant qu'hôte, nous la suivons donc.


Il faut dans un premier temps initialiser le cœur. On active différents masques et interruptions à travers différents registres ainsi que les modes :

- Host negotiation protocol (HNP) qui permet au cœur de changer dynamiquement son rôle de périphérique A-Host en A-esclave et inversement ou B-host en B-esclave.

- Session request protocol (SRP) qui permet, lorsque la calculatrice est un hôte USB, d'économiser de l'énergie en éteignant le Vbus power si la session USB est suspendue


On procède ensuite à l'initialisation du mode hôte. On active le port de contrôle et le registre de statut, on sélectionne le mode de vitesse supportée. Si un périphérique USB est détecté, on active le processus de reset sur le périphérique, on se règle sur sa vitesse et on configure la taille des queues FIFO pour l'échange des données.


Il faudra ensuite procéder à l'initialisation d'un canal de communication pour que la calculatrice puisse échanger avec le robot.

Semaine 3

  • Établissement de la communication entre la calculatrice et le robot :

Initialisation des channels : afin de pouvoir communiquer avec les périphériques connectés à l'hôte, en l’occurrence ici, l'Arduino à la calculatrice, il faut initialiser des canaux de communication. Pour cela, on active les canaux voulus à travers les registres, on active certaines interruptions et masques. Il faut finalement donner une taille au canal en fonction des paquets attendus ainsi que donner les caractéristiques du endpoint que l'Arduino présentera.

Maintenant que l'initialisation a été modifié pour que la calculatrice apparaisse comme un hôte USB, il faut adapter les fonctions de communication. J'ai alors passé pas mal de temps à essayer de comprendre l'architecture du logiciel en terme de communication USB et chercher ce qu'il y a à modifier.

Semaine 4

  • Établissement de la communication entre la calculatrice et le robot :

Après avoir passé du temps à essayer de trouver les modifications à apporter aux fonctions de communication USB du logiciel Numworks, j'ai découvert que STMicroelectronics propose des bibliothèques pour aider à la programmation de leurs microcontrôleurs. On trouve dans ces dernières des fonctions toutes faites pour la configuration de l'USB pour un mode hôte ou esclave. En plus de proposer des fonctions réalisant l'initialisation du core, du mode hôte et des canaux de communication comme j'ai pu le faire au cours des deux dernières semaines, ils proposent des fonctions de communication ainsi qu'une interface d'abstraction matérielle permettant de faciliter la portabilité. J'ai donc choisi d'utiliser ces bibliothèques plutôt que de continuer à réaliser les fonctions moi-même et de risquer de faire des erreurs qui seraient sûrement difficiles à trouver au moment des tests. Cela me permettra aussi probablement de gagner du temps.

Après avoir programmé le logiciel Numworks pour utiliser la fonction d'initialisation de l'USB fournie par les bibliothèques STM, je suis confronté à un problème d'intégration au vu des erreurs apparaissant à la compilation.

Semaine 5

  • Établissement de la communication entre la calculatrice et le robot :

- Résolution du problème de la semaine 4. La semaine dernière, en voulant intégrer les bibliothèques que propose STMicroelectronics j'ai été confronté à un problème à la compilation. Après avoir utilisé un type défini par les bibliothèques intégrées dans le code du logiciel Numworks afin de faire de la calculatrice un hôte USB, j'avais une erreur à la compilation qui m'indiquait que le type utilisé n'était pas défini bien que le fichier .h était lié. J'ai alors, dans un premier temps, pensé qu'il s'agissait d'un problème au niveau des makefile suite à l'ajout des bibliothèques. Finalement, en examinant le code des .h des bibliothèques STM, je me suis rendu compte qu'ils contenaient presque tous le code suivant :

  #if defined(STM32F405xx) || defined(STM32F415xx) || defined(STM32F407xx) || defined(STM32F417xx) || \
   defined(STM32F427xx) || defined(STM32F437xx) || defined(STM32F429xx) || defined(STM32F439xx) || \
   defined(STM32F401xC) || defined(STM32F401xE) || defined(STM32F411xE) || defined(STM32F446xx) || \
   defined(STM32F469xx) || defined(STM32F479xx) || defined(STM32F412Zx) || defined(STM32F412Vx) || \
   defined(STM32F412Rx) || defined(STM32F412Cx) || defined(STM32F413xx) || defined(STM32F423xx)

Les bibliothèques proposées par STMicroelectronics sont communes à chacun de leurs microcontrôleurs. Cependant, ces derniers étant tous différents, il existe des différences au niveau des registres. Afin de pouvoir utiliser les bibliothèques, il faut donc définir le modèle de microcontrôleur utilisé dans un .h bien particulier. Sans cela, le if defined étant présent en tête de fichier et ne se fermant qu'à la fin, tout le contenu était ignoré. Après avoir défini l'utilisation du STM32F412VGT6 dans le .h adéquat, le problème de typage non reconnu à la compilation a été résolu.

Après avoir inclus les nombreux .h différents nécessaires pour l'utilisation des fonctions d'initialisation du périphérique USB, j'ai été confronté à un autre problème :


Compilation suite à l'ajout des bibliothèques STM


De nombreuses variables sont définies sous le même nom dans le logiciel Numworks et dans les bibliothèques STM mais sont utilisées différemment et dans deux langages (C++ pour Numworks, C pour bibliothèques STM). Des problèmes de collision apparaissent donc parmi d'autres à la compilation. Les variables en question étant utilisées dans de nombreux fichiers différents dont le code n'est pas forcément simple à comprendre sans l'étudier et y passer du temps, réadapter le code des bibliothèques à celui du logiciel demanderait beaucoup de temps. J'ai donc choisi de revenir sur la solution de départ et de réaliser les fonctions nécessaires moi-même en prenant appui sur ces bibliothèques.

- J'ai donc dans un second temps, repris les fonctions d'initialisation créées au cours des semaines dernières pour les réutiliser et je les ai ajusté en fonction de l'implémentation que STMicroelectronics propose dans ses bibliothèques.

Semaine 6

  • Conception du robot  :

Après avoir passé pas mal de temps sur l'établissement de la communication, j'ai décidé de laisser cela de côté pour cette semaine, j'y reviendrai plus tard. J'ai dans un premier temps assemblé le châssis du robot, chose relativement simple et rapide avec la notice fournie au kit.


Châssis assemblé
  • Réalisation du shield :

Afin de réaliser un travail propre et qu'il n'y ait pas de fils dans tous les sens, il a été demandé de réaliser un shield pour l'arduino en y intégrant les différents composants utilisés. On intégrera à ce shield :

- Le contrôleur de moteur TB6612FNG

- Des headers permettant de connecter les deux moteurs au contrôleur

- Le capteur de distance HC SR04

La batterie sera connectée au robot par le shield avec des fils.

Les capteurs optiques suiveurs de lignes devant se trouver sous le robot pour être utiles, nous réaliserons un autre PCB pour ces derniers.


J'ai décidé de réaliser ces cartes électroniques avec le logiciel Eagle, notamment parce qu'il est disponible sous Linux contrairement à Altium. Après avoir procédé à l'installation du logiciel, j'ai recherché des librairies Eagle proposant les composants dont j'ai besoin :

- Librairie SparkFun-Boards pour le template de shield Arduino Uno

- Librairie diy-modules pour le capteur de distance HC SR04

- Librairie TB6612FNG,C,8,EL pour le contrôleur de moteur TB6612FNG

- Librairie con-headers-jp pour les headers femelles

- Librairie SparkFun-PowerSymbols pour le 5V et la masse



J'ai ensuite cherché à savoir comment connecter les différents composants entre eux puis procédé à la réalisation du schematics et du PCB associé :


Schematic du shield


PCB du shield

Semaine 7

  • Réalisation du shield :

Les capteurs de ligne devant se trouver en dessous du robot pour être utilisables, j'ai réalisé une seconde carte électronique afin de minimiser le nombre de fils de connexion sur le robot. Cette carte ne comporte que des capteurs suiveurs de ligne ainsi qu'un header pour lier cette dernière au shield.

Pour réaliser cette carte, j'ai utilisé la librairie QRE1113GR qui fourni la footprint du capteur optique QRE1113GR utilisé :


Footprint du capteur optique QRE1113GR


Le schematic de la carte est le suivant :

Schematic de la carte de capteurs optiques pour le suivi de ligne


Finalement, le PCB associé :


PCB de la carte de capteurs optiques pour le suivi de ligne



  • Retour de soutenance intermédiaire  :

Suite à la soutenance intermédiaire, les objectifs du projet ont évolué. La modification du logiciel de la calculatrice Numworks étant possible mais demandant beaucoup de temps, cette tâche est abandonnée. A la place, nous utiliserons une solution déjà existante de communication en pur série avec l'Arduino : https://zardam.github.io/post/numworks-uart-over-usb/

Les objectifs sont donc désormais les suivants :

- Robot comme demandé initialement

- Mettre en place un protocole de communication série pour que la calculatrice puisse utiliser les capteurs et les actionneurs du robot

- Réaliser un câble propre pour la connexion entre la calculatrice NumWorks (port micro USB) et l'Arduino (E/S 0 et 1, masse)

- Développer une API python simple pour qu'un lycéen puisse utiliser le robot

- Développer un exemple de programme python sur la NumWorks pour utiliser le robot comme un suiveur de ligne avec arrêt en présence d'obstacles (sujet original)

- Réaliser un démonstrateur de programmation de l'Arduino par la NumWorks : Ajout d'un capteur de température DHT22 sur le robot, saisie de codes assembleur ATMega328p (hexa) sur la Numworks pour la gestion du capteur (un code d'initialisation et un code de récupération de valeur), envoi des deux codes par série sur l'Arduino puis utilisation de ceux-ci.


  • Établissement de la communication entre la calculatrice et le robot :

La communication entre la calculatrice Numworks et l'Arduino doit donc désormais se faire en série grâce à la solution https://zardam.github.io/post/numworks-uart-over-usb/

L'auteur de cette solution a mappé un UART sur les ports D+ et D- de l'USB en modifiant le logiciel Numworks. La communication série est ainsi possible en utilisant le port USB de la calculatrice.

Semaine 8

  • Réalisation du shield :

Suite à l'évolution des objectifs, j'ai été amené à réaliser quelques modifications sur le PCB. Le capteur de température DHT22 a été ajouté, une réorganisation des connexions a été mené afin de permettre un routage plus simple et plus propre et enfin, étant donné que deux digital pins étaient libres sur l'Arduino, deux LEDs ont été ajouté et pourront notamment servir au débogage.

Étant donné que le capteur de température est plus une option qu'autre chose sur le robot, plutôt que d'utiliser directement sa footprint et le souder par la suite, nous utiliserons un header. Ainsi, il pourra être enlevé et remis à souhait.

Le nouveau schématic est le suivant :


Schematic final du shield Arduino


Le PCB associé :

PCB du shield Arduino


  • Établissement de la communication entre la calculatrice et le robot :

Afin que la calculatrice et l'Arduino puissent se comprendre lors de l'échange des messages, il faut définir un protocole de communication. La calculatrice doit pouvoir faire comprendre à l'Arduino les différentes commandes qu'elle veut faire exécuter, par exemple, ordonner au robot de tourner sur la gauche. Dans l'autre sens, la calculatrice doit pouvoir comprendre et interpréter les valeurs des capteurs que l'Arduino lui enverra.

Nous pensons définir un message comme suit :

- Caractère de début de message

- Taille du message

- Commande représentée par un chiffre (Par exemple, lorsque l'on enverra 1, le robot devra avancer tout droit). OU chiffre signifiant réponse de capteur

- Paramètre de commande si nécessaire OU valeur de capteur

- Checksum pour vérifier la bonne réception du message

La taille d'un message et le type de données échangées n'a pas encore été décidé. D'ailleurs, concernant le type de données échangées, une API de communication série Python a été développé par l'auteur de la modification du logiciel Numworks. Cette dernière propose deux fonctions pour l'envoi et la réception qui ont été implémenté comme suit :

  mp_obj_t writeLine(mp_obj_t a) {
    Ion::Console::writeLine(mp_obj_str_get_str(a));
    return mp_const_none;
  }
  mp_obj_t readLine() {
    Ion::Console::readLine(line, 64);
    return mp_obj_new_str(line, strlen(line), false);
  }

Ces deux dernières ayant été implémenté pour réaliser un tchat entre deux calculatrices Numworks, elles travaillent avec des String. Nous verrons si nous décidons d'utiliser ces dernières ou si il est plus judicieux de modifier les types utilisés.

Semaine 9

  • Réalisation du shield :

Modifications sur les cartes compte tenue des remarques des encadrants. Principalement :

- Connexion des AO1/AO1, BO1/BO1, AO2/AO2, BO2/BO2 du contrôleur moteur afin de limiter le courant dans chacune des broches du circuit

- Ajout d'un plan de masse sur chacune des cartes (améliorant le routage)

- Déplacement/rotation de certains composants

- Élargissement des pistes

PCB final du shield Arduino


PCB final de la carte de suiveurs de ligne


  • Établissement de la communication entre la calculatrice et le robot :

La communication entre la calculatrice et le robot se fera par échange de string. Une trame se composera comme suit :

Information

Char de début de trame Commande Paramètres Checksum Char de fin de trame

Nombre de Char

1 2 3 3 1

Cette trame a plutôt été pensé pour l'envoi de commande de la calculatrice vers l'Arduino (implémentée cette semaine), nous nous préoccuperons de la réponse lors de la semaine 10. Le champ paramètre n'est là qu'au cas ou et sera peut-être amené à disparaître pour l'envoi de commande. Pour la réponse, nous pourrions préciser le type de capteur à la place dans le champs Commande et la valeur du capteur en question dans le champs Paramètre ou alors constituer une trame contenant la valeur de tous les capteurs (à réfléchir).


Une commande sera donc représentée par deux Char. La liste des commandes avec l'expression en Char est la suivante :

- LED1 : "01" (Allumer la LED1)

- LED2 : "02" (Allumer la LED2)

- MOVE_FORWARD : "10" (Avancer)

- MOVE_BACKWARD : "11" (Reculer)

- MOVE_RIGHT : "12" (Tourner à droite)

- MOVE_LEFT : "13" (Tourner à gauche)

- STOP : "05" (Arrêter le robot)

- GET_DISTANCE : "06" (Demander la valeur indiquée par le capteur de distance)

- GET_LINEFOLLOWER : "07" (Demander les valeurs indiquées par les capteurs optiques)

- GET_TEMP : "08" (Demander la valeur indiquée par le capteur de température)


  • Développement de l'API  :

La calculatrice permet le développement Python grâce à MicroPython qui est une version de Python pour l'embarqué. Le développement d'une API se fait directement dans le code source MicroPython en C/C++. En plus d'écrire l'API dans un fichier .c ou .cpp comme on le fait traditionnellement, il faut modifier différents fichiers afin que MicroPython puisse reconnaître l'API et les différentes fonctions implémentées ainsi que permettre l'import du module développé.


J'ai choisi de nommer l'API "robot". Il faut écrire un fichier .c définissant la structure du nouveau module :

  STATIC MP_DEFINE_CONST_FUN_OBJ_0(led1_obj, led1);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(led2_obj, led2);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(moveForward_obj, moveForward);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(moveBackward_obj, moveBackward);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(moveLeft_obj, moveLeft);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(moveRight_obj, moveRight);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(stop_obj, stop);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(getDistanceSensorValue_obj, getDistanceSensorValue);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(getTemperatureSensorValue_obj, getTemperatureSensorValue);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(getLineFollowerSensor1Value_obj, getLineFollowerSensor1Value);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(getLineFollowerSensor2Value_obj, getLineFollowerSensor2Value);
  STATIC MP_DEFINE_CONST_FUN_OBJ_0(getLineFollowerSensor3Value_obj, getLineFollowerSensor3Value);
  STATIC const mp_rom_map_elem_t robot_module_globals_table[] = {
    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_robot) },
    { MP_ROM_QSTR(MP_QSTR_led1), (mp_obj_t)&led1_obj },
    { MP_ROM_QSTR(MP_QSTR_led2), (mp_obj_t)&led2_obj },
    { MP_ROM_QSTR(MP_QSTR_moveForward), (mp_obj_t)&moveForward_obj },
    { MP_ROM_QSTR(MP_QSTR_moveBackward), (mp_obj_t)&moveBackward_obj },
    { MP_ROM_QSTR(MP_QSTR_moveLeft), (mp_obj_t)&moveLeft_obj },
    { MP_ROM_QSTR(MP_QSTR_moveRight), (mp_obj_t)&moveRight_obj },
    { MP_ROM_QSTR(MP_QSTR_stop), (mp_obj_t)&stop_obj },
    { MP_ROM_QSTR(MP_QSTR_getDistanceSensorValue), (mp_obj_t)&getDistanceSensorValue_obj },
    { MP_ROM_QSTR(MP_QSTR_getTemperatureSensorValue), (mp_obj_t)&getTemperatureSensorValue_obj },
    { MP_ROM_QSTR(MP_QSTR_getLineFollowerSensor1Value), (mp_obj_t)&getLineFollowerSensor1Value_obj },
    { MP_ROM_QSTR(MP_QSTR_getLineFollowerSensor2Value), (mp_obj_t)&getLineFollowerSensor2Value_obj },
    { MP_ROM_QSTR(MP_QSTR_getLineFollowerSensor3Value), (mp_obj_t)&getLineFollowerSensor3Value_obj }
  };
  STATIC MP_DEFINE_CONST_DICT(robot_module_globals, robot_module_globals_table);
  const mp_obj_module_t robot_module = {
      .base = { &mp_type_module },
      .globals = (mp_obj_dict_t*)&robot_module_globals,
  };


Ce fichier permet de définir le module MicroPython à travers une structure de type mp_obj_module_t. On définit d'abord les différentes fonctions du module sous forme d'objet. On remplit ensuite un dictionnaire avec les différentes fonctions implémentées. Finalement, on initialise les différents champs de la structure qui représentera l'API en lui précisant notamment le dictionnaire contenant les objets de fonction créés.

Afin de permettre l'import de l'API dans un futur code d'utilisation Python, on modifie le fichier mpconfigport.h en y ajoutant la ligne suivante dans la définition des MICROPY_PORT_BUILTIN_MODULES :

  { MP_ROM_QSTR(MP_QSTR_robot), MP_ROM_PTR(&robot_module) }

Afin d'ajouter la reconnaissance par MicroPython des fonctions implémentées dans notre API et de son nom, il faut modifier le fichier qstrdefsport.h. Dans notre cas, on y ajoute les lignes suivantes :

  Q(robot)
  Q(led1)
  Q(led2)
  Q(moveForward)
  Q(moveBackward)
  Q(moveLeft)
  Q(moveRight)
  Q(stop)
  Q(getDistanceSensorValue)
  Q(getTemperatureSensorValue)
  Q(getLineFollowerSensor1Value)
  Q(getLineFollowerSensor2Value)
  Q(getLineFollowerSensor3Value)

MicroPython sera alors capable de reconnaître par exemple que led1 correspond à une fonction définie.

Finalement, on implémente l'API dans un fichier .cpp ou .c comme on le fait traditionnellement.

Cette semaine, j'ai écrit une fonction pour le calcul du checksum qui réalise la somme de tous les chars de la trame. Le checksum sera représenté par 3 chars.

J'ai ensuite écrit une fonction de formation de la trame à envoyer. Cette dernière prend en paramètre la string de deux chars représentant la commande à réaliser ainsi qu'une string de 3 chars représentant les paramètres de commande à associer. Elle réalise alors une string en plaçant en première position un 'S' indiquant le début de trame, en plaçant ensuite la commande, les paramètres, le checksum en faisant appel à la fonction de calcul de checksum puis termine par le caractère de fin de trame 'E'.

J'ai finalement défini les fonctions de commande led1(), led2(), moveForward(), moveBackward(), moveRight(), moveLeft() et stop() qui font appel à la fonction de construction de trame en précisant la commande associée et écrivent sur le port série grâce à la fonction writeLine(mp_objet_t objet) implémentée par Zardam.

Ainsi, pour demander au robot d'avancer, la string envoyée sera la suivante : S13000417E


Lors de l'écriture des différentes fonctions, j'ai été confronté à un des principaux enjeux de l'embarqué : l'optimisation des ressources. La calculatrice présentant un stockage disponible assez faible, les fonctions des librairies standards du langage C non utilisées ont été supprimé afin de d'occuper le moins d'espace possible. Par exemple, la fonction strcat de la librairie <string.h> n'est pas disponible, j'ai alors dû trouver des alternatives.

Semaine 10

  • Réalisation du shield :

Les cartes n'ont pas pu être gravées cette semaine. J'ai eu un retour de Monsieur Flamen vendredi qui m'a indiqué qu'il était impossible de les graver du fait d'une isolation trop faible des pistes par rapport au plan de masse. Si elles avaient été gravé, il y aurait eu des courts circuits partout. J'ai alors modifié ces dernières et j'ai fixé l'isolation des pistes par rapport au plan de masse à 12 mil qui est le minimum à respecter. J'ai été confronté à un problème suite à cette modification sur la carte du shield. Avec une isolation de 12 mil, le plan de masse ne peut pas recouvrir toute la carte et certaines zones de masse sont des zones "orphelines" c'est à dire, non reliées au reste du plan de masse. Ne voyant pas d'autre solution et avec les conseils de Monsieur Flamen, j'ai ajouté des vias pour réaliser la connexion entre les différentes zones de masse. Une fois la carte gravée, ces zones seront alors connectées par un fil sur la face top.


  • Conception du robot :

Ayant avancé sur la partie programmation de la calculatrice et de l'Arduino, il me fallait réaliser le câble de connexion pour relier les deux afin de pouvoir réaliser des tests et voir si la programmation réalisée est fonctionnelle ou non. Utilisant une communication série, il me fallait un câble qui puisse être branché d'un côté sur le port micro-USB de la Numworks et de l'autre, sur le port série de l'Arduino. Ce type de câble n'existant pas à ma connaissance, j'ai pris un câble USB - micro-USB dont j'ai coupé la partie USB puis j'ai extrait les quatre fils (GND, VCC, Data+ et Data-) en dénudant.


Fils d'un câble USB


J'ai ensuite soudé un fil de connexion mâle-mâle sur chacun des fils afin de pouvoir les connecter à l'Arduino plus aisément. Enfin, j'ai ajouté de la gaine thermorétractable afin d'avoir un rendu plus propre et plus solide. Je peux désormais établir une connexion série entre la Numworks et l'Arduino avec ce câble en connectant :

- Le fil vert D- sur Tx

- Le fil blanc D+ sur Rx

- Le fil noir GND sur GND

Le fil rouge Vcc ne sera pas utilisé car la calculatrice ne nécessite pas d'alimentation.


  • Établissement de la communication entre la calculatrice et le robot / Développement de l'API :

Ayant écrit les fonctions de l'API Numworks permettant de contrôler les leds et les moteurs la semaine dernière, je me suis occupé cette semaine de la partie Arduino afin de pouvoir commencer à réaliser des tests et voir si l'on arrive bien à communiquer entre l'Arduino et la calculatrice.

- Setup() : Indication Output ou Input pour chaque pin selon le schematic du shield. Baudrate à 115200 car c'est celui utilisé par la Numworks.

- Réalisation d'une fonction de lecture de trame reçue. Tant que l'on reçoit des caractères et que l'on a pas lu le caractère de fin de trame 'E', on les stocke dans un tableau de Char.

- Écriture d'une fonction de découpage de la trame. Afin de faciliter son implémentation, j'ai modifié le format des trames en ajoutant un caractère de séparation '-' entre chaque type de donnée. Les trames sont désormais de la forme : S-01-000-573-E. On utilise alors la fonction strtok pour stocker la commande reçue (dans la trame précedente : "01"), les paramètres ("000") et le checksum ("573") en ayant pris soin de vérifier que le premier caractère de la trame est bien 'S'.

- Écriture d'une fonction de vérification du checksum. On renvoie True ou False selon que la somme des caractères de la trame (sans le checksum) est égale au checksum de la trame ou non

- Écriture d'une fonction de traitement de la commande reçue. Si le checksum de la trame est correct, on exécute la commande associée à l'ordre reçu.

- Écriture de la fonction principale loop(). On lit la trame, on la découpe et on exécute la commande demandée.


Une fois le programme Arduino écrit, j'ai ajouté un script de test dans la Numworks pour ne pas avoir à le réécrire à chaque fois. Pour cela, il y a plusieurs fichiers de l'OS epsilon à modifier.

On définit le script dans le fichier epsilon/apps/code/script_template.cpp :

  const ScriptTemplate * ScriptTemplate::TestRobot() {
   return &testRobotScriptTemplate;
  }
  constexpr ScriptTemplate testRobotScriptTemplate("test_robot.py", R"(import robot
  def test1():
   robot.led1()");

Dans ce script de test, on exécute la fonction led1() qui envoie à l'Arduino une demande d'allumage de la LED déjà présente sur l'Arduino (Pin 13), en utilisant la nouvelle API 'robot'.

On ajoute alors un attribut à la classe ScriptTemplate du fichier epsilon/apps/code/script_template.h :

  static const ScriptTemplate * TestRobot();

Enfin, on rend accessible le script grâce à la fonction addScriptFromTemplate dans le fichier epsilon/apps/code/script_store.cpp

  addScriptFromTemplate(ScriptTemplate::TestRobot());

Le script de test est désormais disponible dans l'application Python de la Numworks sous le nom : test_robot.py. Il n'y a plus qu'a exécuter le script et appeler la fonction définie dans ce dernier.


Après cela, j'ai pu tester la communication entre la Numworks et l'Arduino. Dans un premier temps, l’exécution du script sur la calculatrice provoquait un arrêt de celle-ci qu'elle soit connectée ou non à l'Arduino. La seule solution pour récupérer le contrôle de la calculatrice était alors le bouton reset. J'ai alors pensé à un problème mémoire. Utilisant des mallocs dans l'implémentation de l'API, j'ai vérifié s'il n'y avait pas de fuite mémoire grâce à Valgrind. Ce n'était pas le cas, l'utilitaire n'indiquait aucun problème. Finalement, le problème venait d'une mauvaise utilisation des types MicroPython lors de l'écriture sur le port série grâce à la fonction WriteLine(mp_objet_t objet). Après avoir corrigé ce problème, j'ai pu vérifier le bon fonctionnement de la communication. Lors de l'exécution du script, la LED de l'Arduino s'allume bien.

Semaine 11

  • Réalisation du shield :

Les cartes ont été gravées. Tous les composants ont été soudé. J'ai passé pas mal de temps sur la soudure des headers qui ne prenait pas. Je remercie d'ailleurs M Flamen pour l'aide ainsi que la solution qu'il m'a apporté. Après avoir essayé lui même, il s'est résolu à apporter de l'ancien étain de chez lui qui a été beaucoup plus efficace et a finalement permis de souder les composants traversants.



Une fois les soudures réalisées, j'ai pu tester les deux cartes.

Le shield ne semble pas fonctionner, lors de l'ajout de ce dernier à l'Arduino, il n'est plus possible de téléverser de programme, l'Arduino ne répond plus :


Arduino ne répondant plus après ajout du shield


Cela provient probablement d'un court-circuit sur le shield apparu lors des soudures. La résolution de ce problème sera l'objectif principal de la semaine prochaine.

Quant à la carte de suiveurs de lignes, elle est fonctionnelle en la connectant directement à l'Arduino, je suis capable de récupérer les valeurs renvoyées par les différents capteurs.


  • Établissement de la communication entre la calculatrice et le robot / Développement de l'API :

Cette semaine, je me suis occupé de la partie échange de valeurs des capteurs. Je garde au final le même format de trame pour la réponse, la valeur du capteur en question occupera le champs "paramètre" qui ne servait pas jusqu'à maintenant. Dans un premier temps, du côté de l'Arduino, j'ai ajouté :

- Une fonction de formation de la trame de réponse qui est sensiblement la même que celle écrite pour l'envoi de commande sur la Numworks. La différence notable est qu'elle prend ici en paramètre la valeur du capteur à envoyer.

- Une fonction de calcul de checksum qui est la même que celle utilisée pour l'envoi de commande sur la Numworks

- Une fonction pour les suiveurs de ligne qui récupère la valeur du capteur, la donne à la fonction de formation de réponse puis envoie la réponse sur le port série. La récupération de la valeur du capteur se fait juste par lecture du pin analogique associé au capteur suiveur de ligne voulu.

- Une fonction pour le capteur de distance qui récupère la valeur du capteur, la donne à la fonction de formation de réponse puis envoie la réponse sur le port série. Pour récupérer la valeur du capteur de distance, il faut d'abord envoyer une pulsation sur TRIGGER pour lancer une mesure. Un ultrason est alors lancé et est réfléchit par l'obstacle si il y en a un avant de revenir au capteur. La valeur lue sur le pin ECHO est alors le temps mis par l'onde pour aller jusqu'à l'obstacle puis revenir. On divise donc cette valeur par deux puis on utilise la relation d = v*t pour finalement avoir la distance séparant le robot de l'obstacle.

- Ajout de la prise en compte des demandes de valeur du capteur de distance ou des suiveurs de ligne dans la fonction de traitement de la commande reçue.

Les fonctions citées précédemment permettent bien d'envoyer une réponse valide sur le port série. Il faudra par contre vérifier si la lecture des capteurs est correcte une fois que le shield sera utilisable.

Dans un second temps, j'ai ajouté sur la Numworks :

- Une fonction de lecture de trame. Pour lire sur le port série, on utilise la fonction implémentée par Zardam readLine(). On définit alors une variable globale pour le stockage de la trame que l'on remplit en récupérant la chaîne de caractères qui arrive sur le port série mp_obj_str_get_str(readLine())

- Une fonction de découpage de la réponse reçue. Contrairement à la fonction de parsing implémentée sur l'Arduino, je n'ai pas pu utiliser strtok pour le découpage, cette fonction n'étant pas présente dans epsilon. La fonction strchr est par contre disponible et c'est cette dernière qui m'a permis de réaliser le parsing. En localisant les caractères de délimitation '-' par strchr, je peux déplacer le pointeur sur le caractère suivant et ainsi récupérer les caractères qui m'intéressent avec strlcpy.

- Une fonction de récupération de la valeur du capteur de distance. Dans un premier temps, on envoie à l'Arduino la trame demandant la valeur du capteur de distance grâce aux fonctions implémentées lors des semaines précédentes. Une fois la demande envoyée, on utilise la fonction de lecture puis de parsing pour récupérer la valeur demandée. La valeur récupérée étant sous forme de string, on utilise la fonction atoi pour obtenir la valeur en int. Finalement, on renvoi la valeur sous forme d'entier MicroPython : return mp_obj_new_int(value);

- Une fonction de récupération pour chaque capteur suiveur de ligne qui fonctionne exactement de la même façon que la fonction pour le capteur de distance.

Il faudra ajouter à la lecture des réponses une fonction de vérification du checksum.

Après avoir implémenté toutes les fonctions citées précédemment, j'ai pu les tester en définissant moi-même une valeur de capteur. La demande est bien envoyée à l'Arduino qui répond correctement suivant le capteur voulu. La Numworks est bien capable d'isoler la valeur désirée dans la trame reçue est de la renvoyer.

J'ai passé pas mal de temps sur l'écriture des fonctions du coté de la Numworks par rapport à l'utilisation rigoureuse des types MicroPython et à la difficulté de réaliser des tests ainsi que de débugger.

Semaine 12

  • Réalisation du shield :

Le problème du shield a été résolu. Un court-circuit se trouvait sur Rx ce qui expliquait la non communication entre l'Arduino et le PC ou la Numworks après l'ajout du shield. Le shield fonctionne désormais correctement.

  • Établissement de la communication entre la calculatrice et le robot / Développement de l'API :

J'ai ajouté le contrôle du checksum lorsqu'une réponse est reçue sur la Numworks. L'algorithme est le même que sur l'Arduino, on somme tous les caractères de la trame reçue excepté le checksum puis on vérifie si c'est égal au checksum contenu dans la trame reçue.


Maintenant que le shield est fonctionnel, j'ai pu tester si les différents composants fonctionnaient correctement ainsi que les fonctions implémentées de traitement des données des capteurs. J'ai alors été confronté à un problème lors de l'utilisation du capteur de distance. Lorsque j'utilisais la fonction implémentée en demandant une lecture de capteur de distance à partir de la calculatrice, j'obtenais tout le temps une valeur nulle. En revanche, lorsque j'utilisais la fonction implémentée dans un programme à part sur l'Arduino, les valeurs reçues étaient correctes. Après avoir passé quelques heures à chercher la cause du problème, je me suis rendu compte qu'il était causé par une double utilisation de la fonction pinMode dans deux modes différents pour un des pins du capteur de distance.


Une fois que l'utilisation de chacun des composants du robot était fonctionnelle, j'ai tenté de réaliser plusieurs échanges à la suite entre l'Arduino et la Numworks dans l'optique de développer le programme de suivi de lignes. Par exemple, en écrivant un script Python demandant au robot d'avancer puis de s'arrêter lorsqu'il se trouve à moins de 100 mm d'un obstacle :

  import robot
  def test():
    robot.moveForward()
    while(1):
      if (robot.getDistanceSensorValue() <= 100):
        robot.stop() 
        break

Je me suis alors rendu compte qu'il était impossible d'effectuer deux échanges à la suite entre l'Arduino et la Numworks, avant même que l'Arduino n'avait traité la première commande, la Numworks avait déjà envoyé la suivante ce qui provoquait une incompréhension entre les deux. Pour résoudre ce problème, j'ai décidé de mettre en place une validation du traitement d'une commande reçue. Sur l'Arduino, lorsqu'une commande à effectuer ne nécessite pas de réponse pour la Numworks, par exemple, la commande de moteurs ou de led, l'Arduino envoie désormais "OK" à la Numworks lorsque la commande a été traité. Du côté de la Numworks, lorsque l'on demande à l'Arduino de réaliser une action ne nécessitant pas de réponse, on attend d'avoir reçu la validation "OK" par l'Arduino avant de retourner. Ainsi, la Numworks attend désormais que l'Arduino ait traité la commande avant d'en envoyer une autre, il n'y a plus de problème de synchronisation.


  • Développement du programme Python  :

Une fois qu'il eut été possible de chaîner les échanges, j'ai tenté d'établir le programme exemple de suivi de lignes avec arrêt en cas d'obstacle. Avant même d'arriver bien loin dans l'implémentation de ce dernier, je me suis rendu compte que le temps de traitement des échanges allait être un problème pour le suivi de ligne. J'ai alors modifié les fonctions moteurs sur l'Arduino pour que le robot se déplace lentement et ainsi avoir plus de temps pour traiter les informations échangées. Cependant, cela risque de ne pas suffire, en utilisant le script mentionné plus haut pour la détection d'obstacle, le robot qui est censé s'arrêter lorsqu'un obstacle se trouve à 10cm de lui, ne s'arrête qu'à 2 ou 3 cm de l'obstacle.

Semaine 13

  • Conception du robot :

Réalisation manuelle d'un support en bois pour la calculatrice afin de pouvoir la poser sur le robot :

Support pour la Numworks


Il s'avère cependant que le support réalisé est assez lourd et gêne le déplacement du robot par son poids.


  • Établissement de la communication entre la calculatrice et le robot / Développement de l'API :

Suite au problème rencontré de la lenteur des échanges d'information entre la calculatrice et le robot, j'ai revu la façon de récupérer les valeurs des capteurs. On envoyait jusqu'à présent une trame par capteur. Afin de réduire un peu les échanges, j'ai implémenté une nouvelle commande qui demande la valeur de tous les capteurs. L'Arduino renvoie alors la valeur du capteur de distance et des trois suiveurs de ligne dans une seule trame de réponse constituée comme suit :

Information

Char de début de trame Capteur de distance Suiveur de ligne gauche Suiveur de ligne centre Suiveur de ligne droit Checksum Char de fin de trame

Nombre de Char

1 3 3 3 3 4 1


Après test, je me suis rendu compte que cela n'améliorait pas énormément le temps mis par la calculatrice et le robot pour s'échanger des données. J'ai alors cherché à trouver la source du problème. En laissant la Numworks de côté et en envoyant les trames à la main sur le port série à partir du PC, le temps mis pour récupérer la trame de réponse envoyée par l'Arduino était le même qu'en envoyant la trame par la Numworks. Le problème ne venait donc pas de la formation des trames. Il s'agissait en faite de la fonction de lecture de trame implémentée sur l'Arduino :

 void lireTrame() {
   if (Serial.find("S-")) {
   Serial.readBytes(serialBuffer,14);
   strcpy(trame, "S-");
   strlcpy(trame + strlen(trame), serialBuffer, strlen(serialBuffer)+1);
   trame[15] = '\0';
   }
 }

Après l'avoir remplacé par la suivante :

 void lireTrame() {
   int nbCharLus = 0;
   while (Serial.available() > 0 && nbCharLus <= 14) {
     trame[nbCharLus] = Serial.read();
     nbCharLus++;
     delay(5);
   }
   trame[nbCharLus] = '\0';
 }

Le temps d'échange est devenu nettement plus court et m'a redonné de l'espoir vis à vis du suivi de ligne.


  • Développement du programme Python :

Dans un premier temps, j'ai décidé de développer le suiveur de ligne sur l'Arduino seule afin d'avoir un programme dont je suis sûr du fonctionnement et d'éviter certains problèmes. Je me suis vite rendu compte qu'un simple algorithme comme :

  Tant que 1 : 
    Si obstacle : 
      s'arrêter
    Sinon si capteur centre sur la ligne : 
      avancer tout droit
    Sinon si capteur gauche sur la ligne :
      tourner à gauche
    Sinon si capteur droit sur la ligne : 
      tourner à droite
  Fin tant que

Ne me permettrait pas de passer les angles droits. J'ai alors abouti sur l'algorithme suivant :

  Tant que 1 : 
   Si obstacle : 
     s'arrêter
   Sinon si capteur centre sur la ligne ET capteur G et D pas sur la ligne : 
     avancer tout droit
   Sinon si capteur G sur la ligne ET capteur D pas sur la ligne :
     tourner à gauche
     derniereAction <- gauche
   Sinon si capteur D sur la ligne ET capteur G pas sur la ligne: 
     tourner à droite
     derniereAction <- droite
   Sinon si capteur G ET capteur D sur la ligne : 
     Si derniereAction == droite :
       Tourner à gauche
       derniereAction <- gauche
     Sinon si derniereAction == gauche :
       Tourner à droite
       derniereAction <- droite
   Sinon si capteur G et D et C pas sur la ligne : 
     Si derniereAction == droite :
       Tourner à droite
     Sinon si derniereAction == gauche :
       Tourner à gauche
 Fin tant que


Une fois l'algorithme fonctionnel, j'ai tenté de le porter sur la Numworks. J'ai alors passé de nombreuses heures sur un problème qui, au final, s'avèrait assez bête. Lorsque j'utilisais la batterie sur l'Arduino pour le suivi de ligne sans la Numworks, il n'y avait aucun problème. En revanche, en utilisant la Numworks, le capteur de distance renvoyait toujours des valeurs fausses et le robot s'arrêtait alors qu'il n'y avait pas d'obstacle devant lui. Lorsque l'Arduino était alimentée en série par le pc, il n'y avait plus de problème. Après m'être cassé la tête sur le problème et avoir tout tenté, je me suis résolu à changer les piles... ce qui a résolu le problème. Après quelques ajustements, le suivi de ligne avec arrêt lors de la rencontre d'un obstacle est fonctionnel en commandant le robot par la Numworks.

Documents Rendus

- Gitlab du projet : https://archives.plil.fr/mcreteur/NumworksEtRobot.git

- Code version zip : NumworksEtRobot

- Schematics, routage et fichiers Gerber des cartes réalisées : Fichiers cartes électroniques

- Rapport de projet

Bibliographie

- https://zardam.github.io/post/numworks-uart-over-usb/

- https://github.com/zardam/epsilon/tree/uart_over_usb

- https://github.com/numworks/epsilon

- https://www.numworks.com/

- https://www.st.com/content/ccc/resource/technical/document/user_manual/b8/5a/28/c2/cf/b6/47/d6/DM00105256.pdf/files/DM00105256.pdf/jcr:content/translations/en.DM00105256.pdf

- https://www.st.com/content/ccc/resource/technical/document/user_manual/1c/6b/06/e6/19/6c/46/bf/CD00289278.pdf/files/CD00289278.pdf/jcr:content/translations/en.CD00289278.pdf

- https://www.st.com/content/ccc/resource/technical/document/reference_manual/group0/4f/7b/2b/bd/04/b3/49/25/DM00180369/files/DM00180369.pdf/jcr:content/translations/en.DM00180369.pdf

- https://peip-ima.plil.fr/mediawiki/index.php/BE_2017-2018

- http://aaroneiche.com/2010/06/24/a-beginners-guide-to-making-an-arduino-shield-pcb/

- https://www.open-electronics.org/how-to-make-an-arduino-shield-with-eagle-cad-tutorial/

- https://docs-emea.rs-online.com/webdocs/10e2/0900766b810e25b7.pdf

- https://www.sparkfun.com/datasheets/Robotics/TB6612FNG.pdf

- https://forum.micropython.org/viewtopic.php?t=776

- https://micropython-dev-docs.readthedocs.io/en/latest/adding-module.html

- https://www.snapeda.com/parts/QRE1113GR/Fairchild%20Semiconductor/view-part/

- https://www.snapeda.com/parts/POLOLU-713/Pololu/view-part/

- http://www.diymodules.org/eagle-show-object?type=dm&file=diy-modules.lbr&device=ULTRASONIC-HC-SR04

- https://odpf.org/images/archives_docs/17eme/memoires/gr-5/memoire.pdf

- https://www.techiedelight.com/implement-itoa-function-in-c/