···87878888== Installation du plugin dans Gazebo
89899090-Un _system plugin_ Gazebo consiste en la définition d'une classe héritant de `gz::sim::System`, ainsi que d'autres interfaces permettant notamment d'exécuter notre code avant ou après une mise à jour de l'état du simulateur (avec `gz::sim::ISystem`{`Pre`,`Post`}`Update`)
9090+Un _system plugin_ Gazebo consiste en la définition d'une classe héritant de `gz::sim::System` et d'interfaces permettant notamment d'exécuter notre code avant ou après un pas de temps du simulateur (avec `gz::sim::ISystem`{`Pre`,`Post`}`Update`)
91919292-#dontbreak(
9292+#figure(
9393+ caption: [Fichier header pour le plugin],
9394 ```cpp
9495 #include <gz/sim/System.hh>
9596 namespace gz_unitree
···115116```cpp
116117#include <gz/plugin/Register.hh>
117118118118-... // implementation
119119+... // class implementation
119120120121GZ_ADD_PLUGIN(
121122 UnitreePlugin,
···123124 UnitreePlugin::ISystemPreUpdate)
124125```
125126126126-Enfin, on active le plugin en le référançant dans le fichier SDF @sdf-plugin, qui décrit l'environnement du simulateurs (objets, éclairage, etc)
127127+Enfin, on active le plugin en le référençant dans le fichier SDF @sdf-plugin, qui décrit l'environnement des simulations (objets, éclairage, etc)
127128128129#zebraw(
129130 numbering: false,
···152153- L'interaction avec les canaux DDS du SDK d'Unitree
153154- Les données et méthodes internes au plugin
154155155155-En plus de cela, il y a bien évidemment la politique de contrôle $Pi$, qui interagit via les canaux DDS avec le robot (qu'il soit réel, ou simulé)
156156+En plus de cela, il y a bien évidemment la politique de contrôle $Pi$, qui interagit avec le robot (qu'il soit réel, ou simulé) via le SDK, et donc via les canaux DDS.
156157157158#let legend = (
158159 ..descriptions,
···232233 name: <dds>,
233234 (<channelfactory>, <publisher>, <subscriber>),
234235 alignment: top + center,
235235- )[SDK d'Unitree]
236236+ )[Unitree SDK]
236237237238238239 node(name: <gzclock>, (1, 5), subtitled(
···265266266267 if show-legend {
267268 node((0, 5), stroke: none, width: 15em, fill: white, legend(
268268- ("--", "Message DDS"),
269269- ("..", "Message Gazebo"),
269269+ ("-->", "Message DDS"),
270270+ ("..>", "Message Gazebo"),
270271 ("@->", "Désynchronisation"),
271272 ))
272273 }
···306307- Un _publisher_, chargé d'envoyer périodiquement des messages sur `rt/lowstate` en appellant la méthode `LowStateWriter`
307308- Un _subscriber_, chargé d'appeller la méthode `CmdHandler` avec chaque message arrivant sur `rt/lowcmd`.
308309309309-On démarre aussi deux autres #emph[subscriber]s, qui sont eux chargés d'écouter des messages sur les topics Gazebo `/clock` et `/imu`, ce qui permet de récupérer le tick de simulation et les valeurs du capteur IMU#footnote[Inertial Measurement Unit, appelée "Centrale intertielle" en français], que l'on a préalablement fixé au modèle du robot en le déclarant au fichier SDF chargé par Gazebo. Le capteur IMU donne des informations importantes sur la position et la vitesse dans l'espace du robot.
310310+On démarre aussi deux autres #emph[subscriber]s, qui sont eux chargés d'écouter des messages sur les topics Gazebo `/clock` et `/imu`, ce qui permet de récupérer le tick de simulation et les valeurs du capteur IMU#footnote[Inertial Measurement Unit, appelée "Centrale intertielle" en français], que l'on a préalablement fixé au modèle du robot en le déclarant dans le fichier SDF chargé par Gazebo. Le capteur IMU donne des informations spatiales importantes sur la position et la vitesse du robot.
310311311311-Les topics Gazebo sont un autre moyen de communcation inter-processus asynchrone par pub/sub, similaire à DDS @gz-topics. Gazebo utilise Protobuf @protobuf pour définir les types des messages @gz-messages, qui joue ici le même role qu'IDL dans DDS. Les topics Gazebo sont basés sur un réseau de noeuds décentralisé, chaque noeud pouvant publier et/ou recevoir des messages.
312312+Les topics Gazebo sont un autre moyen de communcation inter-processus asynchrone par pub/sub, similaire à DDS @gz-topics. Gazebo utilise Protobuf pour définir les types des messages @protobuf @gz-messages, qui joue ici le même role qu'IDL dans DDS. Les topics Gazebo sont basés sur un réseau décentralisé de nœuds, chaque nœud pouvant indépendamment publier et/ou recevoir des messages.
312313313313-Cette initialisation est faite à l'initialisation du plugin par Gazebo, en la faisant dans la méhode `::Configure` du plugin.
314314+Cette initialisation est faite à la phase de configuration du plugin par Gazebo, via l'implémentation de la méthode `::Configure` du plugin.
314315315315-En pratique, on utilise `std::bind` @cpp-bind pour fixer l'instance d'`UnitreePlugin` et ainsi passer des méthodes de la classe comme des simples fonctions
316316+En pratique, on utilise `std::bind` @cpp-bind pour fixer l'instance d'`UnitreePlugin` et ainsi passer des méthodes de la classe comme des simples fonctions.
316317317317-#grid(
318318- columns: 2,
319319- gutter: 1em,
320320- figure(
321321- caption: [Création d'un _subscriber_ à `rt/lowcmd` dans `UnitreePlugin::Configure`],
322322- text(size: 0.8em, ```cpp
323323- auto subscriber = ChannelSubscriberPtr<LowCmd_>(
324324- new ChannelSubscriber<LowCmd_>("rt/lowcmd")
325325- );
326326-327327- auto handler = std::bind(
328328- &UnitreePlugin::CmdHandler,
329329- this,
330330- std::placeholders::_1
331331- )
318318+#figure(
319319+ caption: [Création d'un _subscriber_ à `rt/lowcmd` et d'un _publisher_ sur `rt/lowstate` dans `UnitreePlugin::Configure`],
320320+ text(size: 0.8em,
321321+ grid(
322322+ columns: 2,
323323+ gutter: 1em,
324324+ ```cpp
325325+ auto subscriber = ChannelSubscriberPtr<LowCmd_>(
326326+ new ChannelSubscriber<LowCmd_>("rt/lowcmd")
327327+ );
332328333333- subscriber->InitChannel(handler, 1);
329329+ auto handler = std::bind(
330330+ &UnitreePlugin::CmdHandler,
331331+ this,
332332+ std::placeholders::_1
333333+ )
334334335335- .
336336- ```),
337337- ),
335335+ subscriber->InitChannel(handler, 1);
336336+ ```,
337337+ ```cpp
338338+ auto publisher = ChannelPublisherPtr<LowState_>(
339339+ new ChannelPublisher<LowState_>("rt/lowstate")
340340+ );
338341339339- figure(
340340- caption: [Création du _publisher_ pour `rt/lowstate` dans `UnitreePlugin::Configure`],
341341- text(size: 0.8em, ```cpp
342342- auto publisher = ChannelPublisherPtr<LowState_>(
343343- new ChannelPublisher<LowState_>("rt/lowstate")
344344- );
342342+ publisher->InitChannel();
345343346346- publisher->InitChannel();
347347-348348- this->publisher_thread = CreateRecurrentThreadEx(
349349- "low_state_writer",
350350- UT_CPU_ID_NONE,
351351- 500,
352352- &UnitreePlugin::LowStateWriter,
353353- this
354354- );
355355- ```),
356356- ),
357357-)
344344+ this->publisher_thread = CreateRecurrentThreadEx(
345345+ "low_state_writer",
346346+ UT_CPU_ID_NONE,
347347+ 500,
348348+ &UnitreePlugin::LowStateWriter,
349349+ this
350350+ );
351351+ ```
352352+)))
358353359354== Calcul des nouveaux couples des moteurs
360355···387382388383Cette équation met à jour $tau$ pour rapprocher l'état actuel du moteur de la nouvelle consigne, en prenant en compte
389384390390-- L'erreur sur l'angle $Delta q$ (partie "proportional")
391391-- L'erreur sur la vitesse de changement de $Delta q$ (partie "derivative")
385385+- L'erreur sur l'angle $Delta q$ (partie proportionelle).
386386+- L'erreur sur la vitesse de changement de $Delta q$ (partie dérivative). Cette prise en compte de la vitesse permet de lisser les changements appliqués aux moteurs.
392387- Un couple dit de _feed-forward_, $tau_"ff"$, qui permet le maintient du robot à un état stable. On pourrait le déterminer en lançant une première simulation, avec pour objectif le maintient debout. Une fois la stabilité atteinte, on relève les couples des moteurs. Intuitivement, on peut voir $tau_"ff"$ comme un manière de s'affranchir de la partie "maintient debout" dans l'expression de la commande, similairement à la mise à zéro ("tarer") d'une balance.
393388394394-Cette prise en compte de la vitesse permet de lisser les changements appliqués aux moteurs
395395-396396-On contrôle la proportion de chaque terme dans le calcul de la nouvelle consigne grâce à deux coefficients, $K_p$ et $K_d$.
389389+On contrôle la prépondérance des deux erreurs dans le calcul de la nouvelle consigne grâce à deux coefficients, $K_p$ et $K_d$.
397390398391399399-400392== `rt/lowcmd` <receive-lowcmd>
401393402402-En pratique, les valeurs actuelles pour le calcul de $Delta q$ et $Delta dot(q)$ proviennent de l'état du moteur, accessible dans `rt/lowstate` avec les champs `q` et `dq` du moteur en question @h1-rt-lowstate
403394404404-#figure(
405405- caption: [Implémentation de la mise à jour de $tau$],
406406- ```cpp
407407- // Avec i l'indice du moteur
408408- auto force = cmdbuf->tau_ff.at(i) + // tau_ff
409409- cmdbuf->kp.at(i) * ( // K_p
410410- cmdbuf->q_target.at(i) - lowstate.motor_state().at(i).q() // Delta q
411411- ) +
412412- cmdbuf->kd.at(i) * ( // K_d
413413- cmdbuf->dq_target.at(i) - lowstate.motor_state().at(i).dq() // Delta q.
414414- );
415415-416416- std::vector<double> torque = {force};
417417- joint.SetForce(ecm, torque);
418418- ```,
419419-)
420420-421421-Cette équation se rapproche des modèles de type PID (_proportional-integrative-derivative_) @control-pid, avec le terme intégratif remplacé par $tau_"ff"$, ce qui en fait une expression plus adaptée pour les politiques avec des mouvements non-brusques: le terme intégratif apporte une capacité d'instabilité qui complexifie l'entraînement #refneeded
422422-423423-// === Réception des commandes <receive-lowcmd>
424424-425425-Lorsqu'un message, publié par $Pi$ (1A) et contenant des ordres pour les moteurs, arrive sur `rt/lowcmd`, `ChannelSubscriber` appelle `::CmdHandler` (2, 3), et modifie un _buffer_ (4) contenant la dernière commande reçue.
426426-427427-On trouve dans ces messages les champs nécéssaires à au calcul de $tau$ @h1-rt-lowcmd comme décrit précédemment:
395395+On trouve dans les messages `rt/lowcmd` les champs nécéssaires à au calcul de $tau$ @h1-rt-lowcmd comme décrit précédemment:
428396429397#let greyedout = content => text(fill: luma(120), emph(content))
430398#let undocumented = greyedout[Non documenté]
···452420 [Respectivement $q$, $dot(q)$, $tau_"ff"$, $K_p$ et $K_d$],
453421)
454422423423+Cette équation se rapproche des modèles de type PID (_proportional-integrative-derivative_) @control-pid, avec le terme intégratif remplacé par $tau_"ff"$, ce qui en fait une expression plus adaptée pour les politiques avec des mouvements non-brusques: le terme intégratif apporte une capacité d'instabilité qui complexifie l'entraînement #refneeded
455424456456-Ensuite, Gazebo démarre un nouveau pas de simulation. Avant de faire ce pas, il appelle la méthode `::PreUpdate` sur notre plugin, qui vient chercher la commande stockée dans le _buffer_ (1B), et applique cette commande sur le modèle du robot, animé par le simulateur.
425425+426426+#figure(
427427+ caption: [Implémentation de la mise à jour de $tau$],
428428+ ```cpp
429429+ // Avec i l'indice du moteur
430430+ auto force = cmdbuf->tau_ff.at(i) + // tau_ff
431431+ cmdbuf->kp.at(i) * ( // K_p
432432+ cmdbuf->q_target.at(i) - lowstate.motor_state().at(i).q() // Delta q
433433+ ) +
434434+ cmdbuf->kd.at(i) * ( // K_d
435435+ cmdbuf->dq_target.at(i) - lowstate.motor_state().at(i).dq() // Delta q.
436436+ );
437437+438438+ std::vector<double> torque = {force};
439439+ joint.SetForce(ecm, torque);
440440+ ```,
441441+)
442442+443443+// === Réception des commandes <receive-lowcmd>
444444+445445+Lorsqu'un message, publié par $Pi$ (1A) et contenant des ordres pour les moteurs, arrive sur `rt/lowcmd`, `ChannelSubscriber` appelle `::CmdHandler` (2, 3), et modifie un _buffer_ (4) contenant la dernière commande reçue. Ensuite, Gazebo démarre un nouveau pas de simulation. Avant de faire ce pas, il appelle la méthode `::PreUpdate` sur notre plugin, qui vient chercher la commande stockée dans le _buffer_ (1B), et applique cette commande sur le modèle du robot, animé par le simulateur.
457446458447459448#architecture([Phase de réception des commandes], {
···484473On notera que (1B) s'exécute _parallèlement_ au reste des étapes: la boucle de simulation de Gazebo est indépendante de la boucle de mise à jour de la politique.
485474486475487487-/ Si `::PreUpdate` est plus fréquente: Le simulateur appliquera simplement plusieurs fois la même commande, le buffer n'ayant pas été modifié.
476476+/ Si `::PreUpdate` est plus fréquente: Le simulateur appliquera plusieurs fois la même commande, le buffer `cmdbuf` n'ayant pas été modifié.
488477489489-/ Si `::PreUpdate` est moins fréquente: Certaines commandes seront simplement ignorées par Gazebo, qui ne vera pas la valeur du buffer avant qu'il change de nouveau.
478478+/ Si `::PreUpdate` est moins fréquente: Certaines commandes seront ignorées par Gazebo, qui ne vera pas la valeur du buffer `statebuf` avant qu'il change de nouveau.
490479491480// L'initialisation du subscriber se fait pendant l'initialisation du plugin, c'est à dire dans `UnitreePlugin::Configure`. On relie la réception d'un message à une fonction, qui est ici une méthode, `UnitreePlugin::CmdHandler`.
492481//
···594583 `imu_state…`,
595584 "struct.",
596585 [Valeurs des capteurs intertiels du robot],
597597- [Messages `gz::msgs::IMU` sur le topic Gazebo `/imu` sur le modèle],
586586+ [Messages `gz::msgs::IMU` sur le topic Gazebo `/imu`],
598587599588 ` .quaternion`,
600589 $RR^4$,
601590 [Posture dans l'espace du robot, dans l'ordre $(w, x, y, z)$],
602602- [$w$, $x$, $y$ et $z$ sur `.orientation()`],
591591+ [`w`, `x`, `y` et `z` sur `.orientation()`],
603592604593 ` .rpy`,
605594 $RR^3$,
···610599 $RR^3$,
611600 [Gyroscope],
612601 [
613613- En utilisant les valeurs de `.orientation()`: \
614614- $"atan"_2(2(w x + y z), 1 - 2 (x^2 + y^2) )) \ "asin"(2 (w y - z x)) \ "atan"_2(2(w z + x y), 1 - 2(y^2 + z^2))$
602602+ En utilisant les valeurs de `.orientation()`:
603603+ #math.equation(numbering: none, block: true, $ vec(
604604+ delim: #("[", "]"),
605605+ gap: #0.5em,
606606+ "atan"_2(2(w x + y z), 1 - 2 (x^2 + y^2) ) ,
607607+ "asin"(2 (w y - z x)) ,
608608+ "atan"_2(2(w z + x y), 1 - 2(y^2 + z^2))
609609+ ) $)
615610 ],
616611617612 ` .accelerometer`, $RR^3$, [Accéléromètre], `.angular_velocity()`,
···623618624619 ` .mode`,
625620 ${0, 1}$,
626626- [$0$ pour "Brake" et $1$ pour "FOC#footnote[Field-Oriented Control]", deux modes de contrôle pour le moteur électrique],
621621+ [Modes de contrôle pour le moteur électrique. $0$ pour "Brake" et $1$ pour "FOC#footnote[Field-Oriented Control]"],
627622 [0],
628623629624 ` .q`,