IMA5 2019/2020 P17 : Différence entre versions
(→Semaine 6 : 10-16 Février) |
(→Tests fréquence récupération des positions) |
||
Ligne 450 : | Ligne 450 : | ||
[[Fichier:Webfbrassar2.gif |center|700px]] | [[Fichier:Webfbrassar2.gif |center|700px]] | ||
+ | |||
+ | [[Fichier:Webfbrassar3.gif]] | ||
===package.json=== | ===package.json=== |
Version actuelle datée du 19 février 2020 à 16:22
Sommaire
- 1 Présentation générale
- 2 Préparation du projet
- 3 Réalisation du Projet
- 3.1 Prélude : 1-5 Janvier
- 3.2 Semaine 1 : 6-12 Janvier
- 3.3 Semaine 2 : 13-19 Janvier
- 3.4 Semaine 3 : 20-26 Janvier
- 3.5 Semaine 4: 27 Janvier - 2 Février
- 3.6 Semaine 5 : 3-9 Février
- 3.6.1 Boutons Run/Stop
- 3.6.2 Récupération de la configuration
- 3.6.3 Affichage avec configuration
- 3.6.4 Détection changement de configuration
- 3.6.5 Affichage de la configuration
- 3.6.6 Fonctionnalités supplémentaires à implémenter
- 3.6.7 Nommer la session d'enregistrement
- 3.6.8 Suppression des enregistrements
- 3.6.9 Modification détection changement de configuration
- 3.7 Semaine 6 : 10-16 Février
- 3.8 Semaine 7 : 17-23 Février (Soutenance)
- 4 Documents Rendus
Présentation générale
Sujet : Outil d'analyse de mouvements d'interaction
Etudiant : François Brassart
Encadrants : Laurent Grisoni & Valentin Beauchamp
Description
L'équipe de recherche MINT souhaite réaliser une expérience engageante de l'utilisation de la réalité virtuelle dans la rééducation afin de répondre aux besoins des patients et de leurs thérapeutes. L'objectif est d'optimiser les protocoles de réadaptation, d'accélérer le rétablissement des patients et faciliter leur réintégration dans la vie quotidienne. Cette expérience s'adresse en particulier à la réadaptation des enfants touchés par des maladies chroniques ainsi qu'aux adultes frappés de fatigue et de douleur. Le patient portera un casque de réalité virtuelle et pourra jouer à un jeu, choisi par le médecin via son téléphone. Durant le jeu et à la fin, le thérapeute aura accès à un enregistrement et une analyse des mouvements du joueur sur son téléphone.
Objectifs
Mon projet se concentre sur la partie récupération et transmission des mouvements du joueur. L'objectif est de concevoir un système qui comporte un module d'enregistrement des gestes d'interaction. Une réflexion sera menée sur la manière optimale d'envoyer et de stocker les données utiles à la description des capacités et de la fatigue des participants. En fonction de l'avancement de mes travaux, il sera possible d'élaborer un modèle de fatigue gestuelle en traitant et en analysant les données récoltées. Des réunions avec des médecins peuvent être organisées pour retranscrire une analyse optimale.
Préparation du projet
Cahier des charges
Le médecin doit pouvoir avoir accès, via une interface web, à un enregistrement des mouvements d'interaction du patient qui a joué avec le casque de réalité virtuelle. Les mouvements d'interactions comprennent la position et l'orientation du joueur dans l'espace à minima, et si possible les mouvements des manettes. Il est important de choisir un mode de stockage des enregistrements optimal : il faut que chaque enregistrement soit associé au jeu, niveau de difficulté, date et durée de la session...
Choix techniques : matériel et logiciel
Le matériel a déjà été choisi par l'équipe. Il s'agit notamment d'un casque de réalité virtuelle Occulus Quest et d'une RaspberryPi. Le jeu VR a été créé avec le logiciel Unity.
Liste des tâches à effectuer
- Déterminer comment récupérer les mouvements à partir du jeu et du casque de réalité virtuelle (RV).
- Envoyer ces données sur la raspberry avec un mode de "stockage" adapté (pistes : XML? JSON?).
- Rendre disponibles ces informations sur un serveur Web pour que le médecin puisse y avoir accès.
En fonction de mon avancement et de la communication avec les médecins :
- Déterminer avec les médecins des analyses de mouvements pertinentes.
- Automatiser ces analyses.
Calendrier prévisionnel
Etant parti en semestre à l'étranger, je dispose de 7 semaines pour réaliser ce projet (du 6 au 23 février 2020).
Feuille d'heures
Après la première rencontre avec M. Grisoni, j'ai intégré les bureaux de l'équipe MINT et m'y rends tous les jours de la semaine de 10h à 18h environ.
Réalisation du Projet
Prélude : 1-5 Janvier
Wiki
Création du Wiki et de son squelette, rédaction de la partie "description" du projet.
Unity
Installation et prise en main du logiciel Unity. Unity est un moteur de jeu très répandu. Il permet de développer des jeux 2D, 3D et RV. Il permet de créer des jeux multi-plateformes en C# (ou en Javascript mais le C# est utilisé lors de ce projet).
Recherche de tutos qui seraient utiles pour l'utilisation de ce logiciel dans le cadre de mon projet.
Liens utiles : Liste tutos Unity Tuto Unity Cours en ligne Unity VR HTC Vive HTC Vive Tuto
Semaine 1 : 6-12 Janvier
En attendant de rencontrer M. Grisoni pour préciser le cahier des charges, je me forme sur le logiciel Unity.
Tuto1 : Roll a ball
Notes sur ce tuto : Le C# est utilisé pour réaliser des scripts permettant le mouvement et l'interaction des objets du jeu avec l'utilisateur.
Tuto2 : VR Unity
Tuto3 : VR Unity : Dans scripts pour détecter mouvement : Input.GetAxis("Horizontal"); ou Input.GetAxis("Vertical");
Rencontre avec M. Grisoni et son équipe
J'ai rencontré M. Grisoni à l'IRCICA (Institut de Recherche sur les Composants logiciels et matériels pour l'Information et la Communication Avancée) pour fixer le cahier des charges de mon projet. Il s'avère que depuis la proposition du sujet et mon retour de l'étranger, l'équipe de recherche a avancé sur le projet. Ainsi, des petites modifications du sujet initial [1] ont été réalisées. L'équipe dispose maintenant d'un prototype de jeu fonctionnel. Il s'agit d'un jeu en 2D (Tetris par exemple) qui se commande via des boutons en 3D. L'objectif est que le patient appuie correctement sur les bons boutons qui sont situés dans l'espace à des endroits difficiles d'accès en fonction de sa pathologie (à droite s'il a des difficultés de motricité du bras droit par exemple).
Le jeu a été créé sur Unity et il est exécuté sur une RaspberryPi. La RaspberryPi créée un hotspot wifi sur lequel est connecté le casque. De plus, elle est configurée avec plusieurs serveurs. Le premier envoie les images du jeu au casque de réalité virtuelle. Le second reçoit les boutons qui ont été appuyés par l'utilisateur provenant du casque. Il fait également le lien avec une interface web qui permet d'avoir un mode collaboratif (multijoueur) : le 1er joueur joue avec le casque et le second interagit avec lui depuis son smartphone. Enfin, le 3e serveur fait le lien avec une page web qui permet de configurer la difficulté du jeu et envoyer cette configuration au casque. Les différentes possibilités de configuration actuellement sont :
- gamepadOn : activation ou non du gamepad (manette virtuelle dans le casque)
- oculusControllerOn : activation ou non des manettes oculus
- gamepadAnimation : animation du gamepad
- nbGamePad : nombre de gamepad (entre 1 et 3 manettes virtuelles dans le casque)
Dans la version actuelle, le médecin n'a aucun retour sur les performances du patient. On souhaite donc lui donner accès à un enregistrement des mouvements du joueur dans un premier temps. Ainsi, le médecin aura accès à ces enregistrements depuis une interface web. Cela permettra d'avoir une version "bêta" à présenter à des médecins. Si le temps me le permet, je pourrais être en contact avec eux pour comprendre quelles informations et analyses des mouvements ils ont besoin. On enverra donc sur l'interface web non plus un enregistrement des mouvements mais une analyse des performances du patient (nombre de tentatives pour appuyer sur un bouton par exemple). La partie "enregistrement des mouvements" est la partie principale et nécessaire de mon projet. La partie "analyse des mouvements" est facultative et dépendra donc du temps et de la disponibilité des médecins pour avoir un retour sur le projet.
Je peux me rendre à l'IRCICA pour avoir accès à un casque de réalité virtuelle et travailler avec l'équipe quand je le souhaite. On m'a accordé l'accès au Git du projet, hébergé sur le GitLab de Cristal.
Documentation sur l'Oculus Quest
L'Oculus Quest est un casque de réalité virtuelle entièrement autonome avec 2 manettes à six degrés de liberté et fonctionne sur un système Qualcomm Snapdragon 835 sur puce. L'OS utilisé est android 7.1.1.
Les applications qui peuvent être installées sur ce casque VR sont au format apk, le même format que les applications Android.
Etude du code
Dès que j'ai eu accès au Git, j'ai commencé par essayer de comprendre le code déjà effectuéet ainsi avoir un aperçu global de ce qui a été réalisé et comment cela a été fait. J'ai également préparé mon "environnement de travail" en installant une machine virtuelle linux sur mon ordinateur, installant le logiciel Unity et en clonant les dépôts git. Le projet comprend 2 dépots git : Unity-asset et serverpi.
Rappel Git
Pour éviter les erreurs de git, deux branches "monitoring" ont été créées sur les projets.
Récupération des dossiers git :
git clone ... git checkout monitoring #switcher vers la branche monitoring
Attention à pusher mon travail au bon endroit :
git branch #vérifier la branche actuelle git push origin monitoring #pusher sur la branche monitoring
Formation sur C# et framework .NET
Sur Unity, les scripts sont rédigés en C#. J'ai donc décidé en ce début de projet d'apprendre les bases du C# et du framework .NET. Cela me servira probablement dans mes futures expériences professionnelles. Cours OpenClassrooms
Projet Unity
La scène créée dans Unity, visible dans le casque de RV consiste en une pièce 3D, un écran sur lequel est affiché le jeu 2D (Tetris par exemple) et une grande manette avec des boutons sur lesquels il faut appuyer pour interagir avec le jeu. Le jeu en 2D est exécuté sur la RaspberryPi. Le flux vidéo est envoyé sur le casque RV. Les boutons appuyés sont envoyés sur la RapsberryPi. J'explore les différents scripts afin de comprendre comment l'appui des boutons est détecté.
Les scripts sont rédigés en C#. Chaque script déclare une classe et est associé à un objet dans la scène virtuelle. Les classes possèdent des méthodes de base Start() qui est exécutée lors de l'apparition de l'objet auquel la classe est rattachée, et Update(), qui est exécutée à chaque frame (chaque rafraîchissement de l'image). Il est évidemment possible d'ajouter des nouvelles méthodes et variables.
Récupération des mouvements des manettes
En cette fin de semaine, je me concentre sur la recherche un moyen de récupérer les mouvements des manettes et du casque. Je commence Documentation Oculus Developper
Position de la manette : retourne un vecteur (classe Vector3).
OVRInput.GetLocalControllerPosition(OVRInput.Controller.RTouch)
Angle de la manette : retourne un quaternion.
OVRInput.GetLocalControllerRotation (OVRInput.Controller.RTouch)
OVRInput.Controller.RTouch designe la manette droite et OVRInput.Controller.LTouch désigne la manette gauche. Attention, ces positions seraient relatives à la position d'origine. Question sur un forum demandant script qui crée un objet a position de la manette
Ces fonctions seront à tester.
Semaine 2 : 13-19 Janvier
Premier Script
Je commence cette 2e semaine de projet en élaborant une première version d'un script qui me permettrait de récupérer les mouvements de l'utilisateur. Ainsi, mon script est une classe qui dérive de la classe de base MonoBehaviour. En effet, d'après la documentation Unity [2], "MonoBehaviour is the base class from which every Unity script derives."
Dans la version du jeu actuelle, la manière de détecter qu'un bouton est appuyé par l'utilisateur au moyen d'une manette fonctionne grâce à un système de collision. En effet, Unity permet de détecter quand 2 objets entrent en collision, dans le cas présent une manette et un bouton. Etant donné que je souhaite mapper tous les mouvements des manettes, cet aspect de collision ne m'est pas utile, à moins de créer des objets invisibles dans tout l'espace 3D, de détecter la collision avec une manette et envoyer la position de l'objet. Ce n'est pas la solution la plus optimale.
Grace aux fonctions GetLocalControllerPosition et GetLocalControllerRotation, je peux récupérer les positions et rotation des manettes. Je vais d'abord vérifier que ces dernières soient connectées : IsControllerConnected() or GetActiveController().
Récupération de la position du casque
OVRCameraRig.centerEyeAnchor ( ) Always coincides with average of the left and right eye poses.
position = overCameraRig.centerEyeAnchor.position;
a tester, pour voir ce que cela renvoie. Je dois également voir tous l'intervalle de temps entre lequel je récupère la position.
Réunion avec l'équipe
Mardi 14 janvier à 14h, l'équipe organise un "Whiteboard and Cookies" autour du projet. L'idée est d'avoir une discussion libre autour d'un sujet. Le sujet de la semaine portera sur les différents scénarios que l'on pourrait mettre en place pour le projet ainsi que des techniques de monitoring qu'on pourrait mettre en place. Cette réunion m'a permis de rencontrer toute l'équipe du projet et de mieux comprendre l'objectif global de ce projet de recherche. Pour l'instant l'objectif est de préparer une discussion avec des médecins, thérapeutes en leur proposant un prototype fonctionnel. Plus précisément dans ma partie du projet, je vais commencer par uniquement afficher les logs des mouvements réalisés par les patients sur une interface téléphone. Suite aux discussions avec des médecins, une analyse pertinente de ces mouvements pourra être réalisée (en fonction du calendrier car la rencontre avec les médecins n'est pas encore planifiée). J'ai également eu l'occasion de tester un premier prototype fonctionnel du jeu.
Script Unity
documentation .NET : TcpClient : Avec .Net en C#, Il existe une classe TcpClient qui permet de se connecter à un serveur TCP. Ainsi, je peux créer mon script qui se connecte à un serveur TCP du Raspberry sur un port donné et envoyer les données utiles. Je dois me renseigner sur le type des données que renvoient les fonctions de récupération des mouvements afin de les convertir en un format qui fonctionne avec les fonctions de TcpClient. Par exemple pour envoyer des données je peux utiliser :
byte[] sendBytes = Encoding.UTF8.GetBytes("#chaine de caractères"); stream.Write(sendBytes, 0, sendBytes.Length);
Je vais donc convertir les positions en chaine de caractères.
Serveurs RPi
Les serveurs actuellement existants sur la Raspberry Pi sont codés en Javascript avec Node.js. Je commence par me former succinctement grâce à ce cours
Serveur Web
J'ai ainsi pu créer un serveur en utilisant socket.io et express. Socket.io est une bibliothèque JavaScript pour les applications Web en temps réel. Il permet une communication bidirectionnelle en temps réel entre les clients Web et les serveurs. Express.js est un framework pour construire des applications web basées sur Node.js. Pour utiliser correctement :
var app = require('express')(); var server = require('http').Server(app); var io = require('socket.io')(server);
(d'après documentation de socket.io). Ainsi, lorsqu'un client se connecte sur le bon port, une page HTML s'affiche.
Le fonctionnement de ce type de serveurs fonctionne avec des signaux. Le serveur peut recevoir un signal et effectuer des actions en fonction de ce signal. Il peut également envoyer un signal aux clients connectés. Les signaux peuvent être accompagnés de donnée. Il existe des signaux de base (connexion, déconnexion...) mais des signaux peuvent être créés et implémentés.
Serveur TCP
J'ai également créé un serveur TCP sur un autre port. Pour cela, j'ai utilisé le paquet Node "Net" et sa fonction Server(), permettant de créer un serveur TCP et de l'associer à un port donné. Le principe est semblable à celui du serveur Web : envoi et réception de signaux prédéfinis (connexion, end...) ou non.
Je l'ai testé en le configurant de manière à ce que lorsqu'un client se connecte, un message s'affiche sur la console. Je l'ai testé avec la commande :
nc localhost <port>
J'ai également testé sur la Raspberry Pi et cela fonctionne. Pour le moment, je choisi le port 8090 pour le serveur Web et le port 8091 pour le serveur TCP. Je fais en sorte que ces valeurs soient modifiables facilement.
Je souhaite maintenant réaliser un Script C# Unity qui connecte le casque au serveur TCP de la RPi.
Test Connexion Oculus
J'ai créé un script qui se connecte au serveur TCP et qui envoie la date une fois connecté. La date devrait ainsi s'afficher sur la page web. Premiers tests avec le casque. Celui-ci n'est pas détecté par l'ordinateur. Pour que le casque autorise l'ordinateur à envoyer des applications : télécharger platform tools puis exécuter dans un shell :
./adb.exe devices
Ensuite autoriser l'accès avec le casque et les manettes.
Pour envoyer la date, j'ai utilisé les fonctions suivantes :
byte[] sendBytes = Encoding.UTF8.GetBytes(DateTime.Now.ToString("HH:mm")); stream.Write(sendBytes, 0, sendBytes.Length);
Cela fonctionne, la date s'affiche sur la page web au lancement du casque.
Semaine 3 : 20-26 Janvier
Test Connexion Oculus
Le code que j'avais réalisé en fin de semaine dernière fonctionne. L'oculus se connecte au serveur TCP de la Raspberry, et envoie la date une fois la connexion établie. Cette date est ensuite affichée sur la page web grâce au code javascript lié à la page HTML. Ainsi, la connexion entre l'oculus et la Raspberry est fonctionnelle, et l'affichage sur la page Web également. J'ai choisi le port 8090 pour la page Web et le port 8091 pour le serveur TCP. Ces ports peuvent être modifiés facilement dans Unity et dans le code du serveur de la Raspberry Pi.
Maintenant, je souhaite créer un bouton sur ma page web qui envoie une demande d'information au casque. Ainsi, je pourrai tester facilement la récupération des positions des manettes et du casque. J'ai donc créé un bouton sur ma page web qui envoie une demande d'update qui est transmise au casque. Pour l'instant, le casque répond l'heure actuelle. Cela fonctionne. Je peux donc passer aux tests des fonctions de récupération des positions.
Récupération des mouvements
Test des fonctions de récupération des positions des manettes : Au lieu d'envoyer la date, j'ai créé un bouton update sur la page HTML qui envoie un message au serveur Web, ensuite transmis au casque grâce au serveur TCP. Le casque répond en envoyant les positions du casque et des manettes.
J'obtiens bien sur la page web les valeurs désirées. Je vais tester en bougeant, regarder si les positions sont relatives ou absolues, voir si la taille du guardian (espace de jeu) modifie les valeurs.
Il apparait que les positions des manettes sont relatives à la position initiale de l’œil central (milieu entre les 2 yeux du casques).
Les positions sont représentées par un vecteur (x,y,z) représentant les coordonnées de l’objet dans l’espace, de manière relative à l’origine (centre de la scène virtuelle). L’orientation est représentée par un quaternion (x,y,z,w). Le w est un scalaire représentant la rotation autour du vecteur (x,y,z).
Je me suis rendu compte que lorsqu'on quitte le jeu, le programme plante. Je vais résoudre cette erreur plus tard.
Bug du jeu 2D
En voulant tester la récupération des mouvements, je me suis aperçu que le jeu ne marchait plus, les commandes des boutons appuyés n'étaient plus transmises. Le jeu affichait uniquement l'écran d'accueil. Pourtant, quand on appuie sur un bouton avec la manette, celui-ci change bien de couleur, montrant que la collision entre la manette et le bouton est bien détectée. Après de longues investigations, affichage de logs sur le casque... Il s'est avéré que le serveur gérant la réception des commandes et le gamepad sur la RaspberryPi avaient planté. Il a "simplement" fallu le redémarrer. Nous avons compris que ce problème était lié à la mise en veille automatique du casque, il sera réglé par un membre de l'équipe.
Stockage des données
MongoDB Node.js Dans un premier temps, je vais récupérer les positions toutes les secondes (ce temps peut être facilement modifié grâce à une variable publique). Les logs seront envoyés du casque à la Raspberry toutes les 10 secondes (de même, temps modifiable facilement). Sur la raspberry, je me renseigne sur MongoDB, base de données noSQL qui stocke sous format json. installation MongoDB
sudo apt-get install mongo-db
Suite à cette commande, le service est automatiquement lancé. Il peut être lancé ou arrêté avec la commande :
sudo service mongodb start/stop
Installation du paquet mongodb pour node.js
npm install mongodb
J'ai réalisé quelques tests d'utilisation de mongoDB pour me familiariser. Je vais maintenant programmer le stockage des éléments envoyés par le casque dans la base de données. Ensuite, je réaliserai une interface web permettant de choisir et d'afficher les données de la base.
Format de BDD
Avec MongoDB, chaque collection de la base de données est un ensemble de documents sous le format json. Voici le format que j'ai choisi pour stocker chaque session :
{ "Date":"...", "Start Time":"...", "Logs":[ { "Time":"...", "Left":"...", "Right":"...", "Head":"..." }, { "Time":"...", "Left":"...", "Right":"...", "Head":"..." } ] }
La date et l'heure de début sont envoyées lors de la connexion du casque au serveur TCP.
Pour insérer dans la base de données :
var newLog= new Object(); newLog.Date="24/02/1997"; newLog.StartTime="14:30:22"; mydb.collection("logs").insertOne(objNew, null, function (error, results) { if (error) throw error; console.log("Le document a bien été inséré"); });
Problème de versions
J'ai commencé à programmer le stockage dans la bdd sur ma Machine Virtuelle Ubuntu. Après avoir obtenu un résultat satisfaisant en testant avec un nc, je teste sur la Raspberry en recevant les données du casque. Le code renvoie des erreurs rapidement. Après investigation, je me suis rendu compte que la version de mongo disponible sur Ubuntu n'est pas la même que sur retropie.
TODO
Voir avec Laurent Grisoni si l'orientation du casque et des manettes sont des données utiles ou non.
public OVRCameraRig overCameraRig; overCameraRig.centerEyeAnchor.rotation //orientation du casque
OVRInput.GetLocalControllerRotation (OVRInput.Controller.RTouch).ToString() //orientation des manettes
Après discussion avec Valentin Beauchamp, membre de l'équipe MINT, je vais intégrer ces données afin de montrer aux médecins l'ensembles des possibilités du système et leur offrir plus de possibilités d'analyse.
Semaine 4: 27 Janvier - 2 Février
Etant malade en début de semaine, je n'ai pas pu aller travailler à l'IRCICA.
Erreur lors d'execution sur Raspberry Pi
Après investigation, j'ai trouvé plus d'informations sur l'erreur que j'avais auparavant :
MongoServerSelectionError: Server at localhost:27017 reports maximum wire version 0, but this version of the Node.js Driver requires at least 2 (MongoDB 2.6)
La version de MongoDB disponible sur Retropie est une ancienne version, plus ancienne que celle disponible sur Ubuntu.Ainsi, le paquet node js "mongo" qui permet au programme javascript de communiquer avec la base de données n'est pas compatible avec cette version de Mongo. Je dois donc installer une version plus ancienne du paquet Mongo de node (version 1." au lieu de 3.6). Des différences dans l'utilisation des fonctions sont à noter et je devrai donc modifier mon code.
LOWDB
Après discussion avec les membres de l'équipe, il s'est avéré que l'utilisation d'une ancienne version du paquet node MongoDB n'est pas forcément la plus adaptée. De plus, MongoDB ne permet pas d'obtenir de véritables fichiers JSON sur la Raspberry Pi. Ainsi, gérer plus simplement des fichiers JSON dans un dossier parait être une bonne alternative, et moins lourde qu'une Base de Données tournant en permanence sur la Raspberri, étant donné que la quantité d'informations à stocker n'est pas énorme.
Après quelques recherches, j'ai découvert qu'il existait un paquet NodeJS intitulé "lowDB", qui permet de gérer des fichiers JSON dans un dossier. lowDB. pour l'installation de lowDB :
npm install lowdb
Attention, le dossier dans lequel sont stockés les fichiers JSON doit exister, sinon une erreur appariait. --> coder une fonction qui crée le dossier si celui-ci n'existe pas au lancement du programme.
Pour créer ou ouvrir un fichier JSON, utilisationde filesync et low :
const adapter = new FileSync('db.json') const db = low(adapter)
Ajouter une association clé/valeurdans le fichier :
db.set(<key>, <value>) .write()
Ajouter une valeur à une clé déjà existante
db.get(<key>) .push(<value>) .write()
Pour chaque enregistrement de session, les logs sont enregistrés dans un fichier json, géré par lowdb. les fichiers sont intitulés "YYYYMMDD_HHMMSS.json" (Y=year, M=Month, D=Day, H=hour, M=Minute, S=Second). Ils sont stockés dans le dossier du chemin DB_PATH (varibale de mon programme facilement modifiable).
Fin de Session
Il apparait un problème lors de la fin du jeu. En effet, il faut que l'enregistrement de la session s’arrête quand le jeu est arrêté par l'utilisateur. Je dois donc modifier le script Unity afin que quand le jeu s’arrête, il envoie une commande permettant de terminer la session et ainsi pouvoir créer une nouvelle session d'enregistrement au prochain lancement du jeu. Sur Unity, il existe une fonction OnApplicationQuit(), qui est exécutée lorsque l'application est quittée par l’utilisateur. Après plusieurs tests de cette fonction, je n'ai pas réussi à la faire fonctionner. Des recherches m'ont mené sur ce forum qui confirme que cette fonction n'est pas compatible avec le casque Occulus GO. Seule la fonction OnApplicationPaused(), qui est appelée quand le jeu est mis en pause fonctionne. Ainsi, une nouvelle session d'enregistrement n'est possible que si le jeu est quitté. Si plusieurs patients jouent à la suite sans quitter le jeu, les données seront enregistrées comme une même session, et donc stockées dans le même fichier JSON. Ceci n'est pas optimal et j'en discuterai avec mes encadrants.
Page Web
Pour la construction de ma page Web, je crée un fichier HTML, associé à un script Javascript.
J'intègre un menu déroulant dans lequel seront affichées les différentes sessions enregistrées, correspondant aux fichiers JSON du dossier DB_PATH. Lors de la connexion d'un client sur le serveur Web, la liste des sessions lui est envoyée. Pour se faire, le serveur utilise la fonction fs.readdir qui permet de lister le contenu d'un dossier et de le stocker dans une liste. A la réception de cette liste, la page web, grâce au script Javascript, affiche la date et l'heure de la session et ajoute dans un tableau tous les logs de cette session.
J'utilise Bootstrap pour avoir une page web esthétique et responsive. Bootstrap est un framework utile à la création du design (graphisme, animation et interactions avec la page dans le navigateur, etc.) de sites et d'applications web. J'ai suivi ce tuto afin d'apprendre les bases.
Gérer quand aucun fichier dans dossier
Lors de l'affichage des fichiers contenus dans le dossier, il y a une erreur quand aucun fichier n'est présent dans le dossier. Je dois donc gérer ce cas. Le cas où le dossier n'existe pas n'est pas possible car au démarrage du serveur, le dossier est créé si il n'existe pas. Ainsi, si la page Web reçoit une liste vide, elle affiche "No Files" dans le sélectionneur.
"Réunion" avec Valentin
Valentin, membre de l'équipe est content de mon travail. Je lui ai demandé si cela posait un problème que l'affichage sur smartphone ne soit pas très optimal, étant donné le nombre de colonnes de données à afficher. Ce n'est pas grave, les médecins regarderont probablement cette page sur ordinateur ou tablette pour un meilleur confort. L'affichage des logs n'est pas très "visuel" mais on ne connait pas encore les analyses que voudront faire les médecins donc on laisse comme ça. (C'était le but premier de mon projet, afficher les logs des mouvements en "dur").
récupération de la config : au début de session, stockage de la config reçue dans un json sous nom "YYYYMMDD_HHMMSS_config1.json". Dans mon json de log, pour chaque log j'associe la config1. Dès qu'un changement de config est détecté (voir comment détecter), création d'un nouveau json de config.
Problème de fin de session --> afficher sur page web un bouton "Run" et un bouton "Stop" pour lancer l'enregistrement. A voir réception de messages sur le casque.
Structure des fichiers JSON final :
{ "Date":"...", "Start Time":"...", "Logs":[ { "Time":"...", "Left":"...", "Left Rotation":"...", "Right":"...", "Right Rotation":"...", "Head":"...", "Head Rotation":"..." "Config":"...", }, { "Time":"...", "Left":"...", "Left Rotation":"...", "Right":"...", "Right Rotation":"...", "Head":"...", "Head Rotation":"..." "Config":"...", }, ] }
TODO début de semaine prochaine
Pour finir la semaine, j'ai ajouté les informations de rotation des manettes et du casque au script du casque et sur le serveur pour qu'elles soient stockées dans les fichiers JSON. J'ai également ajouté les colonnes correspondantes sur la page Web. Le tout fonctionne proprement. Mes objectifs pour le début de la semaine prochaine sont :
- Boutons RUN/STOP sur page HTML --> script du casque doit être capable de recevoir des messages (pour l'instant il envoie seulement) --> thread qui reçoit pour être sûrs de recevoir.
- Récupérer config, stocker dans un JSON et associer aux logs.
- Détecter changement de config, et créer un nouveau JSON correspondant.
Semaine 5 : 3-9 Février
Boutons Run/Stop
Sur la page HTML, j'ajoute 2 boutons Run et Stop. J'utilise bootstrap pour changer la couleur des boutons quand ils sont appuyés et savoir si un enregistrement est en cours ou non. L'appui sur un de ces boutons appelle une fonction du même nom qui émet un message au serveur. Ce message est transmis au casque. Sur le casque, je lance un thread qui permet de recevoir les messages provenant du serveur. Un booléen permet d'activer ou nom la récupération et l'envoi de données. Ainsi, à la réception de la commande "Run", le booléen recording passe à true, les données de début de session (date et heure de début) sont envoyées, permettant ainsi au serveur de créer le fichier JSON correspondant à la session.
Cela est fonctionnel (Hormis la partie configuration que je n'ai pas encore programmée). Je dois encore empêcher l'appui sur le bouton Run quand le casque n'est pas connecté, et rafraîchir la liste des sessions quand on appuie sur le bouton Stop, afin de pouvoir accéder directement au dernier log, et ce sans rafraîchir la page. De plus, si les manettes ne sont pas connectées, ou qu'elles n'ont plus de piles, une erreur apparaît. Je dois régler ce problème.
Quand le casque se connecte au serveur, un message est envoyé à la page web pour indiquer qu'il est connecté et donc autoriser l'appui sur le bouton Run. De même, quand il se déconnecte, un message est envoyé pour arrêter l'enregistrement en cours si il y en a un et empêcher le lancement d'un nouvel enregistrement.
De plus, j'ai ajouté le rafraîchissement de la liste des sessions lors d'un appui sur le bouton stop. Ainsi, dès que l'on arrête un enregistrement, celui-ci est directement sélectionnable sur la page et on peut instantanément visualiser la session. J'ai également trié la liste de sessions disponibles par ordre décroissant afin d'avoir la dernière session tout en haut du sélectionneur.
Récupération de la configuration
Dans le cadre ce projet, il est possible de modifier la configuration du jeu (nombre de manettes pour commander le jeu, mouvement des manettes... et d'autres options qui sont en développement). La configuration est facilement modifiable par les médecins et stockée dans un fichier json de la raspberry. Au démarrage du jeu, le script TCPConfig charge le fichier JSON dans une variable et modifie les objets de la scène du jeu. Pour éviter de lire une deuxième fois le fichier JSON, je modifie le script TCPConfig afin de créer une fonction publique GetConfig() qui renvoie une chaîne au format json de la configuration. Dans mon script TCPMonitoring, j'appelle donc cette fonction et récupère la configuration. Lorsque le casque reçoit la commande RUN (quand le médecin demande le début de l'enregistrement sur la page web), le casque envoie en premier la date et l'heure. Ensuite il envoie donc la configuration récupérée précédemment et son numéro (il s'agit d'un compteur qui permet de différencier les différentes configurations. Ici comme c'est le début de la session d'enregistrement, la configuration initiale aura le numéro 1). Ensuite, à chaque log envoyé, sera associé le numéro de la configuration actuelle. Du coté Raspberry, le serveur TCP reçoit donc en premier la date et l'heure, ce qui lui permet de créer un fichier json (avec lowdb) dont le nom reprend la date et l'heure(YYYYMMDD_HHMMSS.json) . Les fichiers de sessions sont stockés dans le chemin DB_PATH (variable facilement modifiable). Ensuite lors de la réception d'une configuration, celle-ci est stockée dans le dossier DB_CONFIG_PATH, sous le nom YYYYMMDD_HHMMSS_configX.json, ou X représente le numéro de la configuration).
Affichage avec configuration
J'ai choisi que le dossier DB_PATH par défaut soit "/home/pi/monitoring/data. le dossier DB_CONFIG_PATH par défaut est "/home/pi/monitoring/data/config". Ainsi, le dossier de config est situé dans le dossier des logs de sessions. Cela pose donc un problème lorsque l'on veut afficher sur la page web la liste des fichiers contenus dans le dossier de session. En effet, quand j'avais créé cette fonctionnalité, je listais tout le contenu du dossier : tous types de fichiers et dossiers. Je dois donc filtrer pour afficher sur la page web uniquement les fichiers JSON.
J'utilise ainsi le module NodeJS "pathname" et sa fonction "extname", qui étant donné un fichier, renvoie son extension (ou renvoie "" s'il s'agit d'un dossier). Je sélectionne donc uniquement les fichiers .json .
files.forEach(function(file){ //pour chaque élément de ma liste (contenu du dossier) if(path.extname(file)!=".json") files.splice(files.indexOf(file), 1 ); //si ce n'est pas un json, je l'enlève de la liste });
Détection changement de configuration
Il est possible de changer la configuration en cours de partie. Ainsi, je dois associer chaque log à une configuration. Quand une nouvelle configuration est envoyée au casque, c'est un thread du script TCPConfig.cs qui la reçoit. Ainsi, ce thread stocke la nouvelle configuration et passe la variable "needNewConfig" à true. Ainsi, au prochain update de la frame, si cette variable est vraie, la configuration est modifiée en fonction de ce qui a été reçu. Je crée donc dans TCPConfig une fonction publique qui renvoie la valeur de la variable "needNewConfig". Ainsi, je vérifie à chaque frame grâce à cette fonction si la configuration a été modifiée. Si c'est le cas, j'incrémente le compteur de configuration, récupère la configuration, l'envoie au serveur de la RPi et la stocke comme expliqué précédemment.
Affichage de la configuration
Pour avoir un affichage efficace, j'ai eu l'idée d'afficher la configuration correspondant à un log particulier d'une session en cliquant sur le numéro de la configuration en question. Cela ouvrirait un pop-up qui afficherait le contenu de la configuration. Après quelques recherches pour déterminer comment j'allais réaliser une pop-up, j'ai découvert l'existence de "modal" en bootstrap. Il s'agit de pop-up entièrement personalisables qui peuvent être ouvertes ou fermées avec une simple fonction. Ainsi, j'ajoute sur ma page HTML un tableau vide (dans lequel j'ajouterai le contenu de la configuration que je désire afficher), ancré dans des "div" de classe modal. Ainsi, le tableau n'est pas affiché tant que je n'appelle pas dans mon script la fonction suivante (ou ModalConfig est l'id de mon modal) :
$('#ModalConfig').modal();
J'ajoute un bouton dans le modal permettant de fermer le modal en lui ajoutant l'attribut data-dismiss="modal".
Cependant, je remarque en faisant des tests, que ma page se grise quand je clique pour afficher le modal, mais qu'il ne s'affiche pas. Après plusieurs tests et recherches sur le web, j'ai compris que pour que le modal fonctionne avec HTML5, il faut ajouter dans le script :
$('#myModal').on('shown.bs.modal', function () { $('#myInput').trigger('focus') })
Ainsi, mon modal s'affiche bien. Je souhaite donc y remplir le tableau avec le contenu de la configuration. Lors d'un clic sur un numéro de configuration, j'envoie au serveur le nom du fichier de configuration, celui-ci me répond avec le fichier de configuration json correspondant, que j'affiche dans le tableau.
Tout ceci est donc totalement fonctionnel et plutôt intuitif.
Fonctionnalités supplémentaires à implémenter
Après concertation avec Valentin Beauchamp, il serait intéressant d'ajouter :
- la possibilité de donner un nom à la session d'enregistrement (nom du patient par exemple), en plus de la date et l'heure, ce qui permettrait d'identifier plus facilement les sessions.
- la possibilité de supprimer un enregistrement facilement
Nommer la session d'enregistrement
J'ai ajouté un élément HTML de type input afin que le médecin puisse entrer un nom pour la session d'enregistrement. Lors de l'appui sur le bouton Run pour lancer l'enregistrement, le signal "Run" est envoyé au serveur TCP avec le nom "custom", entré dans la zone de saisie. Ainsi, quand lowDB crée le fichier json correspondant à la session, il ajoute au nom de base (avec la date et l'heure) ce nom custom. Ainsi, si le nom d'un fichier json de session devient : YYYYMMDD_HHMMSS_customName.json. Le nom custom est évidemment affiché dans la liste de sessions sur la page Web. Les noms de fichiers de configurations deviennent alors YYYYMMDD_HHMMSS_customName_configX.json. Si aucun nom custom n'est entré dans la zone de saisie, le nom du fichier n'est pas impacté.
Suppression des enregistrements
Pour ajouter la possibilité de supprimer les enregistrements, j'abandonne l'affichage de la liste des sessions en mode menu déroulant au profit d'un mode "tableau". Ainsi, je vais afficher le nom de chaque session, avec en face un bouton sélectionner et un bouton supprimer. Le bouton sélectionner masquera la liste des sessions et affichera les logs de la session sélectionnée comme précédemment. Le bouton supprimera affiche un modal (pop-up de bootstrap) qui demandera une confirmation de suppression. Si confirmation, alors un signal "Remove" avec le nom du fichier sera envoyé à la Raspberry qui supprime le fichier correspondant :
fs.unlinkSync(DB_PATH+file);
Il est important de supprimer également les fichiers de configurations correspondant à la session que l'on désire supprimer. Pour ce faire et supprimer toutes les configurations de la session, je commence par lister les fichiers de configuration se trouvant dans le dossier DB_CONFIG_PATH. J'utilise ensuite une expression régulière qui reprend le nom du fichier à supprimer, oté de l'extension ".json", auquel j'ajoute "_config" suivi d'un nombre et de l'extension ".json". Une fois les bons ficheirs de configuration trouvés, je les supprime.
var regex = RegExp(file.replace('.json',)+"_config[0-9]*.json"); fs.readdirSync(DB_CONFIG_PATH) .filter(f => regex.test(f)) .map(f => fs.unlinkSync(DB_CONFIG_PATH + f));
Cela me permet d'être sûr que tous les fichiers de configuration correspondant au fichier de session sélectionné soient supprimés et qu'il ne reste pas de fichiers résiduels inutiles. Une fois la commande de suppression envoyée au serveur, la page web demande un la liste des sessions au serveur. Ainsi, dès que le fichier est supprimé sur la raspberry, il est également supprimé de l'affichage sur le site web.
Modification détection changement de configuration
Après discussion avec Valentin, il s'est avéré que détecter le changement de configuration grâce à la variable du script TCPConfig n'était pas optimale. En effet, dans ce script, une nouvelle configuration est reçue par un thread qui passe la variable "needNewConfig" à true. C'est seulement à la prochaine frame que ce changement sera effectif. Ainsi il y a un petit temps entre le passage de la variable à true et le changement de configuration. Ce n'est à priori pas impactant pour une récupération des logs toutes les 2 secondes. Cependant, il est plus judicieux d'avoir une variable newConfig dans mon script de monitoring, et qui est modifiée par TCPConfig quand il a effectivement changé de configuration.
Semaine 6 : 10-16 Février
Ce début de semaine sera consacré aux derniers tests, push sur le git et intégration des changements.
Tests fréquence récupération des positions
Plusieurs tests ont été réalisés afin de s'assurer de la fluidité du jeu en fonction du temps entre chaque récupération des données. En récupérant les données toutes les 400ms et envoyant au serveur après 10 récupérations, il n'y a aucun problème de latence sur le jeu. Le temps de 400ms est optimal pour la récupération des positions.
package.json
Pour installer tous les paquets NodeJS, j'utilise la commande suivante :
npm install <nom_du_paquet>
Afin de simplifier le processus d'installation des différents paquets, je créée un fichier "package.json" qui liste les différentes dépendances dont le programme a besoin pour fonctionner. Ainsi, une simple commande
npm install
aura pour effet d'installer tous les paquets nécessaires.
Changement données récupérées
Pendant ma période de travail à l'IRCICA, une nouvelle fonctionnalité a été ajoutée au système. Il s'agit de la possibilité de jouer sans les contrôleurs. En effet, le casque Oculus Quest dispose de plusieurs caméras qui permettent de détecter les mains et ainsi interagir avec le jeu directement avec les mains. Pour que le module d'enregistrement des positions puisse marcher, j'ai changé les données retournées par le casque. Au lieu de récupérer les positions et rotation des contrôleurs, je collecte celles des "Hand Anchor". Il s’agit d’un objet situé au même endroit que les mains. Soit le casque positionne cet objet au niveau des manettes et affiche sur la scène virtuelle une image des manettes, soit le casque détecte la position des mains grâce aux multiples caméras dont il dispose.
Modification affichage de la configuration
Au début de mon projet, les configurations possibles du jeu correspondaient à uniquement 4 paramètres. Ainsi, pour afficher la configuration, j'affichais ces 4 paramètres et leur valeur dans un tableau. Etant donné que 2 étudiants travaillent pour ajouter des options de configuration, le nombre de paramètres a changé. Ainsi, pour garder une compatibilité de l'affichage sur la page web peu importe le nombre et le type de données de configuration, je remplace le tableau par une zone de texte dans laquelle se trouve le fichier de configuration au format JSON.
PM2
Pour la stabilité du système, PM2 est utilisé. PM2 est un gestionnaire de processus pour le moteur d’exécution JavaScript Node.js. Il permet de garder les applications en vie pour toujours et de les recharger sans interruption. PM2 permet également de gérer la journalisation et la surveillance des applications
Rédaction du rapport et préparation de la soutenance
Je vais passer une soutenance devant M. Grisoni, Valentin Beauchamp et quelques membres de l'équipe MINT le jeudi 13 février, en entrainement pour la soutenance finale. Je prépare donc mon rapport et ma soutenance afin d'être prêt. Suite à cette soutenance, quelques modifications mineures sont à faire dans le rapport et la présentation.
Semaine 7 : 17-23 Février (Soutenance)
Suite à la soutenance d'entrainement faite en fin de semaine dernière, je prends en compte les conseils qui m'ont été donnés et modifie des parties de ma présentation. Je termine la rédaction de mon rapport ainsi que la rédaction et la relecture de mon wiki.
Mon code a été documenté expliqué à Valentin Beauchamp, de sorte à faciliter des éventuelles modifications.
Ma soutenance se déroulera le vendredi 21 février à 10h20 en présence de Messieurs Grisoni, Vantroys et Redon. Une démonstration sera réalisée.
Documents Rendus
L'ensemble du projet VR4REHAB est hébergé sur le gitlab Cristal. Néanmoins, voici une archive contenant les fichiers que j'ai créés pour la réalisation de ma partie du projet : Fichier:SourcesP17Fbrassar.zip
Rapport : Fichier:RapportP17Fbrassar.pdf
Présentation : Fichier:PresentationP17Fbrassar.pdf