Introduction
Un des buts de Youpi est de se prêter à diverses démonstrations et animations. On l’a vu [1]en train d’obéir à des commandes saisies via une application Web ou un Minitel, de résoudre tout seul des problèmes comme les Tours de Hanoï, mais quid de le programmer d’une manière accessible au non-informaticiens ?
Eh bien c’est chose possible maintenant, grâce à l’aide indirecte de Google.
Programmation graphique
Avant d’entrer dans le vif du sujet, précisons un peu le but recherché.
Il s’agit de permettre à des non-spécialistes (i.e. non-programmeurs) de définir une logique de comportement du bras (séquence de mouvements, logique de décision, itérations...) sans devoir apprendre un quelconque langage de programmation à base de mots-clé et d’instructions cabalistiques.
C’est typiquement ce que LEGO a popularisé dès 1996 avec son environnement de programmation graphique pour les kits robotiques LEGO Mindstorms RCX, puis NXT et maintenant EV3. Basés sur le moteur LabView de National Instruments, dont la réputation n’est plus à faire dans le domaine professionnel des applications de type contrôle-commande, cet environnement permet au roboticien en herbe de définir un algorithme de comportement de son robot en assemblant des éléments graphiques symbolisant les actions élémentaires et les structures d’exécution. Il se base sur un paradigme temporel et fait usage du pilotage par flux de données également (héritage de LabView). Le résultat est le suivant :
D’autres approches ont vu le jour ces dernières années pour proposer des outils faciles d’accès servant de support à l’initiation à la programmation. Un des plus célèbres est Scratch, fruit de travaux du MIT. A la différence de l’environnement Mindstoms, Scratch met plus l’accent visuel sur la structure des algorithmes, la temporalité étant induite par la juxtaposition verticale des blocs.
Scratch a été exploité par de nombreux développeurs pour l’utiliser comme outil de programmation d’autre chose que des animations et interactions graphiques sur le PC (son domaine de départ), et entre autres des robots. Si on met de côté les applications se contentant de télé-commander un robot [2] depuis le PC exécutant le programme Scratch, ce qui offre un intérêt somme toute limité pour nous, la clé du problème est la génération d’un code source (ou équivalent) à partir de la logique exprimée par l’assemblage de la symbolique graphique.
Un grand nombre d’initiatives, pas toujours heureuses d’ailleurs, a ainsi vu le jour, et il existe par exemple des outils permettant de programmer des cartes Arduino en générant le code C soumis au compilateur depuis les schémas Scratch. Malgré toute la considération que méritent ces réalisations, quasiment toutes présentent les mêmes limitations : en tant qu’applications natives, il faut les installer sur sa machine, et une bonne partie part du principe que l’univers informatique se résume à Windows 🙁 Par ailleurs, développer son générateur de code pour Scratch demande un investissement significatif pour entrer dans cet univers.
Google est ton ami...
... cette fois encore, mais pas avec son moteur de recherche cette fois-ci. On peut toujours les critiquer concernant leur prise de pouvoir sur le monde de l’informatique (comme on a critiqué IBM en son temps) mais il faut bien reconnaître qu’il y a quand même chez eux des gus qui ont oublié d’être bête. Et quand il s’agit de faire chauffer les neurones, que ce soit pour créer des idées nouvelles ou pour finaliser les idées que d’autres ont eues, ils savent y faire.
Une de leur créations récentes (2012 pour être précis, ce qui peut paraitre une éternité dans un monde qui bouge à la vitesse petit ’c’) est Blocky. En gros, prenez le principe de Scratch, mettez-le dans un navigateur Web sous forme de bibliothèque Javascript, rendez plus simple la création de blocs personnalisés et la génération de code, et vous aurez Blockly.
Ca peut d’ailleurs servir aussi bien à générer du code au sens habituel du terme que des données de configuration, application de plus en plus répandue, et dont un parfait exemple est l’éditeur graphique de définition de bloc, faisant partie des Block Developper Tools pour créer un DSL [3] basé sur Blockly.
Et nous y voilà : sachant que Blockly intègre out of the box un générateur de code Python parmi d’autres, quoi de plus tentant que d’y ajouter une collection de blocs pour contrôler notre bras préféré et d’intégrer le tout dans la panoplie d’applications embarquées ? Dont acte.
Blockly 4 Youpi
Partons du résultat en images :
Comment ça marche ?
Tout cela est réalisé sous forme d’une application Python indépendante, comme les autres démos embarquées dans Youpi. Elle implémente un serveur HTTP à l’aide de Bottle. Les blocs spécifiques à Youpi sont définis dans un module Javascript, en se basant sur les informations de la documentation développeur disponible sur le site Web de Blockly mentionné plus haut. J’ai choisi de tout définir en Javascript plutôt que d’utiliser JSON pour éviter de multiplier les fichiers source [4]. La toolbox fusionne des blocs standards avec ceux spécifiques à Youpi, et est définie avec le formalisme XML prévu à cet effet.
La partie cliente utilise le classique combo jQuery + Bootstrap, et s’appuie sur quelques services REST fournis par l’application Python pour le contrôle de l’exécution du programme. L’API en question est basée sur les trois points d’entrée suivants :
/run |
POST | lance l’exécution (asynchrone) du programme. Le retour est immédiat et n’attend pas la fin de l’exécution, de manière à ne pas bloquer le client. Si le programme soumis n’est pas valide, un status d’erreur est retourné. Le programme est transmis dans le corps de la requête, sous la forme du code Python généré |
/status |
GET | retourne le status (running, terminated, error,...) actuel du programme |
/abort |
POST | interrompt l’exécution |
La soumission du code Python à exécuter dans la requête peut paraître une hérésie sur le plan sécurité, mais :
- ce code est contrôlé avant exécution, et est rejeté s’il contient des instructions spécifiques telles que
import
, ce qui limite très fortement les possibilités, et les restreint au final aux appels des commandes de Youpi. - notre serveur n’est pas en ligne et exposé à tous les agresseurs, donc pas la peine d’être paranoïaque sans raison valable.
Côté serveur, le modèle d’exécution consiste à ajouter dans le contexte d’exécution passé au built-in exec
de Python un objet faisant office de proxy des commandes du bras. Les méthodes de cet objet sont des wrappers des méthodes natives de l’API de Youpi qu’on a sélectionnées pour être mise à disposition du programme exécuté. Le wrapping consiste à les encadrer par un message de log à toutes fins utiles, mais surtout par un test du signal de demande d’interruption [5]. Histoire que les choses soient moins hermétiques, voici le code du proxy en question :
class ArmProxy(object):
PROXIED_METHODS = ('move', 'goto', 'open_gripper', 'close_gripper', 'go_home', 'move_gripper_at')
def __init__(self, arm, logger, terminate_event):
self.arm = arm
self.logger = logger
self._terminate_event = terminate_event
for method_name in self.PROXIED_METHODS:
setattr(self, method_name, self._wrap(getattr(self.arm, method_name)))
def _wrap(self, method):
def wrapper(*args, **kwargs):
self.logger.info('.. executing %s', method.__name__)
method(*args, **kwargs)
if self._terminate_event.is_set():
self.logger.info('termination signal received')
raise Interrupted()
return wrapper
A noter dans __init__
:
- la construction dynamique de la collection des wrappers des méthodes natives du bras, à partir de la liste des noms de celles à supporter. Python est vraiment génial quand il s’agit de nous éviter le copier/coller et la répétition de code sans intérêt,
- l’utilisation d’une closure dans la méthode
_wrap
qui retourne une fonction construite dynamiquement et basée sur une méthode passée dans son contexte (paramètremethod
).
Afin d’assurer le côté asynchrone et de permettre au client d’interrompre le programme en cours, son exécution est gérée par un runner qui l’active dans un thread. En voici le code :
class Runner(object):
def __init__(self, pgm, arm, panel, logger):
self.pgm = pgm
self.arm = arm
self.panel = panel
self.logger = logger
self._run_thread = None
self._terminated_callback = None
def start(self, on_terminated=None):
if self._run_thread:
raise RuntimeError('already running')
def worker():
self.logger.info('worker started')
try:
exec(self.pgm, {'arm': self.arm, 'panel': self.panel})
except Interrupted:
self.logger.info('worker interrupted')
else:
self.logger.info('worker terminated')
if self._terminated_callback:
self._terminated_callback()
self._terminated_callback = on_terminated
self._run_thread = threading.Thread(target=worker)
self._run_thread.start()
@property
def active(self):
return self._run_thread and self._run_thread.is_alive()
def join(self):
if self._run_thread and self._run_thread.is_alive():
self._run_thread.join(timeout=30)
if self._terminated_callback:
self._terminated_callback()
Conclusion
Je me suis beaucoup amusé à développer cette application, car elle aborde des domaines un peu originaux. Mais surtout parce que ça laisse entrevoir des possibilités fabuleuses basées sur les technologies mises en oeuvre. Ca va très probablement me servir dans mes activités professionnelles, et j’ai déjà quelques idées sur la question.
Pour le code intégral de cette application, rendez-vous sur son repository GitHub.