API REST [poppy-ergo-jr]

ergo-jr
français
Tags: #<Tag:0x00007f5cea0cdf48> #<Tag:0x00007f5cea0cda70>

#1

Bonjour,

J’essaie d’accéder à un poppy ergo-jr via l’api REST (sans passer par les websockets).

En parcourant la documentation et le forum, j’ai trouvé 2 “moyens”:

1/ via le port 6969 (dédié snap) mais les réponses ne sont pas “structurées” dans un json et donc facilement exploitables sans regexp (je préfèrerai éviter…)

2/ via des requêtes HTTP: en lisant la doc pypot, j’ai le format des différentes requêtes mais j’ai systématiquement en retour un code de page html avec le message 404: Not found;

Par exemple, en essayant d’obtenir la liste des moteurs en utilisant l’url http://poppy.local:8080/motor/list.json (dans un browser web ou via la commande curl.

Par contre quand j’utilise simplement http://poppy.local:8080, j’ai en retour un object json très long qui semble contenir l’intégralité des informations sur le robot et son état ce qui me laisse supposer que l’api REST est activée.

Qu’aurai-je raté, non compris?

Merci d’avance,
N.


#2

Bonsoir,
j’ai justement passé un peu de temps ces derniers jours à communiquer en REST avec mon Ergo (pour faire ça, que je documenterai ici bientôt si ça intéresse qqun)
Je me suis basé sur ce post :

et donc des requêtes de la forme
http://poppy.local:6969/motors/set/registers/m6:led:blue
ou
http://poppy.local:6969/motors/set/goto/m2:-10:2

Je ne sais pas si c’est ça que tu cherches.
À noter qu’il faut que le serveur Snap! soit démarré (ce qui est le cas par défaut au démarrage du robot).
et que donc par ex il ne faut pas avoir ouvert un Jupyter sur le robot puisqu’il kille le serveur Snap! (@Theo, arrête-moi si je dis des bêtises)

a+


#3

Bonsoir,

Merci de votre retour,
Oui c’est à votre post sur le sujet auquel je faisais allusion :slight_smile: pour snap et cela fonctionne effectivement bien, enfin en suivant la discussion, j’y arrive bien.

Mais en passant par le port 6969 dédié à snap, les requêtes avec des paramètres, tant en envoi comme dans votre exemple, qu’en retour, se composent d’une chaîne de caractères avec des séparateurs différents (ici ‘:’ mais parfois se sont des ‘;’ ou autres…) Bref que du ‘cas particulier’ à traiter de manières différentes et à traiter avec des expressions régulières…

Or cf. mon post précédent, si j’ai bien compris, l’on peut utiliser des requêtes plus “standard” qui retournent des objects json (je n’ai pas encore vu pour l’envoi) ce qui (dans mon cas) en faciliterai le traîtement.
Or, je n’arrive pas à les atteindre (pourtant elles semblent être actives cf. remarque sur le retour poppy.local:8080 qui est un json très verbeux avec toutes les infos.)

cf. lien doc pypot API rest

En gros, le but serait de m’interfacer avec le robot pour des actions simples (je débute) sans passer par l’éditeur snap et en utilisant des outils sur ma machine: i.e. un éditeur de code textuel un language de mon choix (node/js en l’occurence) et voir le robot comme une boîte noire.

Mais j’en reviens donc à la même conclusion, qu’ai-je non compris raté?

N.


#4

Re-Bonsoir,

au fait, je me suis basé sur la liste des API de la doc ici en utilisant la première requête pour test comme indiqué précédemment).

Or celle-ci retourne effectivement une erreur du type non trouvée.
En essayant simplement les suivantes, je n’ai eu aucun problème, j’obtiens bien les comportements/résultats attendus…

Désolé pour ce bruit, j’ai probablement raté quelque-chose dans la documentation (je ne sais pas si c’est normal sur le poppy-ergo-jr, ma lib pypot qui n’est pas la dernière, etc…)

N.


#5

Bonsoir,
L’API de snap a été conçue pour l’utiliser avec les blocs de snap de base, qui à l’époque ne permettait que des requêtes GET et n’avait pas de décoding de json. Elle est relativement moche, mais elle reste quand même simple à utiliser, il n’y a pas besoin (heureusement) de faire des regex.
Les “;” est “:” sont utilisés comme séparateur, mais comme deux types de séparateur qui n’ont pas la même signification. Pour récupérer la réponses, il suffit de séparer la chaine de caractère par le séparateur désiré.


#6

Bonjour,

Merci de votre retour.

Oui je commence à y voir plus clair et pour l’être moi-même, le but est de se passer de snap car trop “clickodrome” à mon goût pour faire des tests simplement (c’est un point de vue personnel/question d’habitudes et je ne m’y fais pas) et passer par un langage de script afin de “piloter” le robot. Bref faire l’équivalent de snap en textuel… Le tout en utilisant le robot en tant que boîte noire et via les langages et les IDEs dont j’ai l’habitude.

D’où l’utilisation de node/js par convenance personnelle/degré de connaissance qui permet de faire/mapper un ensemble de requêtes REST très simplement et d’utiliser la dualité json <=> object js et donc de le traiter comme bon me semble ensuite les résultats, comme effectuer des boucles comme dans snap :slight_smile: etc…

N.


#7

Bonjour,

Je me permets de ‘remonter’ ce post sur l’utilisation de l’api REST (pas celle de snap, mais l’autre)

Tout est ok, j’arrive à récupérer/“setter” toute les valeurs (numériques et booléennes) sauf pour les leds des moteurs que je n’arrive pas à contrôler (je récupère la valeur courante sans problème).

Quelque soit la requête post que j’effectue, je “bloque” les leds (j’ai un browser avec snap pour transformer mon ergo-jr en sapin de noël/éteindre les leds):

J’ai beau eu faire des essais d’envoi en post d’un chiffre, d’un chiffre en string (“2” par exemple), du code acsii correspondant (pourquoi pas?), au mieux, j’éteins les leds (code 0) mais jamais je n’arrive à en initialiser une à une couleur donnée. D’ailleurs elles s’éteignent toutes ou jusqu’au moteur cible uniquement. Ensuite, je dois rebooter le robot pour que les leds refonctionnent.

Je que j’en ai compris: on doit envoyer un entier compris entre 0 et 7 (la doc des moteurs ici indique que c’est une info sur 3 bits).
En fouillant sur le forum, j’ai pu remonté à un code de la lib pypot et voir que les codes couleurs leds étaient une enumération avec une relation entier<=>string. Par exemple:
‘blue’ => 2 puis 2&0b111 (on ne garde que 3 bits car cela donne toujours 2?) est renvoyé.

Pour info, en utilisant l’api rest qui est dédiée à snap comme explicité ici cela fonctionne sans souci.

Bref je sèche complètement sur ce point.

Merci d’avance,
N.


#8

Bonjour @Nikolaos,

Trés bonne initiative :wink:

effectivement, lors de mes propres tests j’ai remarqué qu’envoyer des valeurs invalides faisait “planter” le registre ‘Led’ du moteur

l’“API de Snap” (pypot/server/snap.py) repose sur L’API REST (pypot/server/rest.py) par l’intermédiaire pypot/server/server.py
@Theo l’expliquerai mieux que moi! Mais, si ca marche en Snap, ça marche sans :wink:

Les valeurs envoyés à l’API pour le registre LED sont : off red green yellow blue pink cyan white
on note ligne 235 que pour tout les registres (sauf pour les leds!) la valeur est transformé en ‘tuple’ puis est transmise à la partie ‘REST’ (ligne 239, fonction ‘set_motor_register_value(motor, register, value)’)

La valeur à transmettre pour l’API REST est donc: off red green yellow blue pink cyan ou white sans aucune modification.

Mais effectivement plus profondément dans le code la valeur est convertie avant envoie au moteur (cf https://github.com/poppy-project/pypot/blob/master/pypot/dynamixel/conversion.py#L317 )


#9

Bonsoir,

Merci de votre retour.

Je ne l’avais pas indiqué, mais effectivement, j’avais aussi essayé en premier avec les valeurs littérales i.e. off, red, etc… avec, par exemple la requête suivante:

curl -X POST -d “green” http://poppy.local:8080/motor/m3/register/led/value.json --header “Content-Type:application/json”

qui me retourne systématiquement une erreur de type 500, Server Error et en html, pas en json.

Les seules valeurs acceptées sont un chiffre i.e. 1 seul caractère entre 0 et 9, et j’obtiens alors le comportement ératique décrit dans mon précédent post.

Or la requête effectuée correspond à celle décrite pour initialiser une valeur du registre (cf. api rest).

Requête qui fonctionne d’ailleurs très bien avec les autres registres mais:

  • ce sont des valeurs entières numériques ou booléennes, non une chaîne de caractère comme dans le cas des led; les “settings” de ma requête sont peut-être mauvais.

Bref je ne peux toujours pas transformer mon ergo-jr en sapin de noël et décembre est derrière nous :disappointed_relieved:

N.


#10

peut-être cette notebook pourrais vous aider:

et ici l’on peut voir quelle sont les url valide et quel fonction est appelé (en l’occurance ‘UpdateMotorRegisterHandler’ )

UpdateMotorRegisterHandler exécute: data = json.loads(self.request.body.decode()) qui utilise le fichier:


mais je n’arrive pas à comprendre pourquoi il arrive à lire le registre correctement ( et le transformer en text cf https://github.com/poppy-project/pypot/blob/011fd757cb201eb7d3008a81e490431f5a9ad2d0/pypot/dynamixel/conversion.py#L317) et pas écrire dessus cf https://github.com/poppy-project/pypot/blob/011fd757cb201eb7d3008a81e490431f5a9ad2d0/pypot/dynamixel/conversion.py#L321

et voici l’erreur retourné par le serveur (via la page http://<robot_name>.local/logs )

‘’‘HTTPServerRequest(protocol=‘http’, host=‘192.168.0.105:8080’, method=‘POST’, uri=’/motor/m3/register/led/value.json’, version=‘HTTP/1.1’, remote_ip=‘192.168.0.108’, headers={‘Content-Length’: ‘5’, ‘\xc3\xa2\xc2\x80\xc2\x9ccontent-Type’: ‘application/json\xc3\xa2\xc2\x80\xc2\x9d’, ‘Content-Type’: ‘application/x-www-form-urlencoded’, ‘Host’: ‘192.168.0.105:8080’, ‘Accept’: ‘/’, ‘User-Agent’: ‘curl/7.52.1’})
Traceback (most recent call last):
File “/home/poppy/miniconda/lib/python2.7/site-packages/tornado/web.py”, line 1413, in _execute
result = method(*self.path_args, **self.path_kwargs)
File “/home/poppy/miniconda/lib/python2.7/site-packages/pypot/server/httpserver.py”, line 123, in post
data = json.loads(self.request.body)
File “/home/poppy/miniconda/lib/python2.7/json/init.py”, line 338, in loads
return _default_decoder.decode(s)
File “/home/poppy/miniconda/lib/python2.7/json/decoder.py”, line 366, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File “/home/poppy/miniconda/lib/python2.7/json/decoder.py”, line 384, in raw_decode
raise ValueError(“No JSON object could be decoded”)
ValueError: No JSON object could be decoded
500 POST /motor/m3/register/led/value.json (192.168.0.108) 14.44ms’’’


#11

Bonsoir,

Je vais rester sur une approche ‘boîte noire’ en ce qui concerne le robot.

J’avais regardé un peu pypot mais c’était dans l’optique de trouver les valeurs à envoyer, pensant que les valeurs littérales n’étaient pas celles à utiliser.

Dans un premier temps, je vais donc me contenter d’un vilain ‘hack’ en ce qui concerne le registre des leds aka je vais passer temporairement pas l’api rest de snap qui fonctionne pour l’initialisation.

N.


#12

Ok, tenez nous au courant de l’évolution de vos problèmes et surtout de vos solutions :wink:


#13

Bonsoir,

Je relance ce post que j’avais créé il y a quelques temps (et un peu abandonné faute de temps à consacrer sur le sujet…)

Pour résumé, afin de remplacer snap qui est trop click-o-drome à mon goût et nuit à mon initiation sur poppy :expressionless: je voulais développer une une solution en mode plus “programmatique” avec la même approche client/serveur ce qui est quasiment le cas via un mode cli et une approche scriptable simple en javascript via node.js/npm.

Basiquement, je me base sur l’API REST exposée par le serveur http pour accéder aux registres des moteurs en lecture/écriture pour créer des commandes basiques, commandes ensuite chaînables ce qui me permet de piloter les moteurs ainsi que de les interroger sur leurs états.

La solution me paraît pas mal, plutôt utile, tout du moins bien adaptée aux réfractaires aux interfaces graphiques/clicks et simple, je tâcherai de la mettre sous github/npm quand certains détails seront réglés (rédiger un ersazt de documentation, certaines parties de code honteuses, etc…)

Mais j’ai encore quelques “problèmes”/incompréhensions auxquels je n’arrive pas à répondre; les voici:

1/ ma première requête suite à la mise sous tension du poppy finit systématiquement en timeout, et ce peu importe la valeur de ce timeout, même grande, sur ma requête, alors que les suivantes passent toujours, même directement enchaînées. Cela donne l’impression que la première requête faite après mise sous tension doit “impliquer” quelque-chose côté serveur (sic) puis le mode de fonctionnement est correct. je pourrais contourner cela en “créant” une fonction de type init() qui lancerait une requête “bidon” afin de palier ce problème, mais j’ai probablement raté quelque-chose,

2/ idem lors de la mise sous tension:

  • je perds systématiquement les valeurs liées à la vitesse (registre ‘moving_speed’ qui est alors à 0,
  • le registre ‘goal_position’ est systématiquement lui à la valeur 150.
    Est-ce que les valeurs ne sont pas persistantes dans les registres? Bref tout est normal (la valeur 150, m’interpelle quand même :thinking: ). Bon d’un autre côté, je ré-initialise tout cela en une petite ligne de commande donc…

3/ J’effectue des rotations sur les moteurs avec une option permettant d’attendre que la position cible soit atteinte (ou non). L’algo est très simple:
a/ j’initialise la valeur ‘goal_position’,
b/ puis, à intervalle temporel régulier (de l’ordre de quelques centaines de ms), j’interroge le moteur cible sur sa position courante (registre ‘present_position’), calcule le delta et en dessous d’une valeur seuil (2degrés, valeurs purement empirique), rends la main.
Mais cela ne fonctionne pas systématiquement et fréquemment, il y a une forte incertitude sur la position courante retournée en “interrogeant” le registre ‘present_position’ suite à un mouvement. L’incertitude est de l’ordre de 5/10 degrés, ce qui est plutôt gênant ici, et il faut “attendre” quelques bonnes secondes après une rotation d’un moteur pour que ce type de requête renvoie la “bonne” valeur pour ce moteur.

Est-ce du aux moteurs xl-320 qualifiés de “low-cost” i.e. la “précision” laisse-t-elle à désirer??
J’ai contourné le problème en calculant un delta entre la position courante à l’itération n et n-1 qui doit être proche de zéro: Il semble que le retour de la requête sur le registre ‘present_position’, même s’il ne retourne pas la position exacte attendue, tends vers une valeur donnée. Cela fait le job mais n’est pas bien “propre” (aka pleinement satisfaisant), j’ai l’impression d’avoir raté quelque-chose…

Bref, quelques problèmes pas insurmontables, plutôt de l’ordre du détail, mais qui me laissent penser que je n’ai pas compris quelques points sur poppy/les moteurs alors que les concepts semblent simples…

Merci d’avance,
N.


#14

Bonjour @Nikolaos,

déjà, je ne pourrais pas répondre à tous, j’espère que @Theo trouvera 5min pour compléter.

Il faut savoir que la valeur des registres est ‘set’ au démarrage dans le fichier /poppy_src/ poppy_ergo_jr/configuration/poppy_ergo_jr.json

Concernant l’aspect low-cost des xl-320 c’est exactement ca, leur précision (et d’autre chose) laisse à désirer, il y a effectivement une marge d’environs +/- 3°
(pour info, un moteur fiable comme la série d’entrée de gamme AX coûte déjà 60€ contre 20€ seulement pour la gamme XL ; l’humanoïde et le Torso sont équipés, pour majorité, de MX28 à 250€ / pièces, mais n’est pas destiné au même usages…)

Je ne sais pas exactement comment contourner ce problème (hormis acheter des moteurs plus onéreux), mais il existe d’ors et déjà une fonction goto_position (https://github.com/poppy-project/pypot/blob/706af9b40f734d89f595dca0c16229b039d34fc7/pypot/dynamixel/motor.py#L243)

def goto_position(self, position, duration, control=None, wait=False):
        """ Automatically sets the goal position and the moving speed to reach the desired position within the duration. """

        if control is None:
            control = self.goto_behavior

        if control == 'minjerk':
            goto_min_jerk = GotoMinJerk(self, position, duration)
            goto_min_jerk.start()

            if wait:
                goto_min_jerk.wait_to_stop()

        elif control == 'dummy':
            dp = abs(self.present_position - position)
            speed = (dp / float(duration)) if duration > 0 else 0

            self.moving_speed = speed
            self.goal_position = position

            if wait:
time.sleep(duration)

peut-être pouvez vous vous en inspirer…

Quand vous dites utilisez l’API, vous utilisé celle du port 8080, celle du port 6969 ou celle du port 9009 ?


#15

Bonsoir,

Merci pour votre retour,

oui concernant les moteurs, j’avais lu cela pour l’ergo-jr qui devait être à prix abordable.

Pour l’api: j’utilise celle exposée via le port 8080 (plus uniformisée que celle de snap ) sauf pour les leds ou j’ai un “vilain” hack (enfin pas si vilain finalement :wink: ) utilisant le port 6969 i.e. celui de snap dans ce cas là.
9009, je ne connais pas; c’est le port par défaut pour les WebSockets?

Pour le goTo, j’ai adapté le test de sortie comme suit:
( abs(present_position - goal_position) < epsilon ) // ce à quoi on s’attend…
|| ( abs(present_position(itération précédente) - present_position ) < 0.5 ) // cf. commentaire à la suite
Même si le retour pour ‘present_position’ n’est pas le bon, la requête semble tendre vers une valeur donnée avant de “ressauter” vers celle de ‘goal_position’… Cela règle le problème tout du moins donne une version acceptable (sauf probablement si la vitesse des moteurs est très très lente mais avec des valeurs du registre ‘moving_speed’ à 50 et plus, cela passe sans problème).

Après il me reste le problème de la première requête REST effectuée après mise sous tension qui sort en timeout systématiquement. Je pense contourner en créant une pauvre fonction init qui:
1/ fait une requête “bidon” sur le robot => je trappe l’exception de timeout,
2/ effectue à la suite 2 requêtes pour initialiser le registre ‘compliant’ à false, puis à true (retour à l’état initial) ce qui a le bon goût d’initialiser la valeur du registre ‘goal_position’ avec celle du registre ‘present_position’ (et donc écraser la fameuse valeur 150).

Quoiqu’il en soit, je m’amuse énormément, sans un click sur une UI! :smiley:

N.


#16

Je pense connaître le besoin de cette requête “bidon”:

au démarrage, par défaut est lancé l’API 6969, la première requête sur 8080 kill celle sur 6969 (pm.stop()) avant de se lancer

tu peux modifier ce qui est démarré au lancement dans ce fichier ligne 70 :
http://poppy.local:8888/edit/poppy_src/puppet-master/bouteillederouge.py
(pm.start() correspond au lancement de l’api 6969)

:heart::heart::heart:


#17

Salut,
moi aussi je déterre ce post, surtout pour partager mes premiers pas avec l’API autour d’un notebook fonctionnel.
Ci-joint le notebook de mes essais
Premier pas Poppy et REST.ipynb (4.8 KB)

A noter que globalement, j’arrive a faire quelques requêtes (merci !)
Mais

  • j’aimerais réussir à commander un moteur et la requete ne passe pas, surement à cause d’un prb de syntaxe.
    -Et la commande d’une primitive renvoie une erreur malgré un positionnement positif de l’ergo.

Si quelqu’un a une idée…

Cordialement,

Sébastien