···11+#import "@preview/zebraw:0.5.5"
22+#import "@preview/fletcher:0.5.8": diagram, node, edge
33+#import "./utils.typ": dontbreak
44+#show figure: set block(spacing: 2em)
55+#let zebraw = (..args) => zebraw.zebraw(lang: false, background-color: luma(255).opacify(0%), ..args)
66+17En se basant sur _unitree\_mujoco_, il a donc été possible de réaliser un bridge pour Gazebo.
2839== Établissement du contact
41055-== Réception des commandes
1111+Une première tentative a été de suivre la documentation de CycloneDDS pour écouter sur le canal @cyclonedds-helloworld `rt/lowcmd`, en récupérant les définitions IDL des messages, disponibles sur le dépot `unitree_ros2`#footnote[`unitree_mujoco` n'avait pas encore été découvert] @unitree_ros2
1212+1313+On commence par importer la bibliothèque DDS et les définitions IDL de `rt/lowcmd`
1414+1515+```cpp
1616+#include "messages/LowCmd_.hpp"
1717+#include "dds/dds.h"
1818+...
1919+2020+int main (int argc, char ** argv)
2121+{
2222+```
2323+2424+On initialise les différents objets permettant de lire sur un canal
2525+2626+```cpp
2727+ dds_entity_t participant, topic, reader;
2828+ LowCmd_ *msg;
2929+ void *samples[MAX_SAMPLES];
3030+ ...
3131+3232+ participant = dds_create_participant(DDS_DOMAIN_DEFAULT, NULL, NULL);
3333+3434+ topic = dds_create_topic(participant, &LowCmd__desc, "HelloWorldData_Msg", NULL, NULL);
3535+3636+ qos = dds_create_qos();
3737+ dds_qset_reliability(qos, DDS_RELIABILITY_RELIABLE, DDS_SECS (10));
3838+3939+ reader = dds_create_reader(participant, topic, qos, NULL);
4040+4141+ dds_delete_qos(qos);
4242+4343+ samples[0] = LowCmd___alloc();
4444+```
4545+4646+Et on attend qu'un message arrive sur le canal, pour l'afficher
4747+4848+```cpp
4949+ /* Poll until data has been read. */
5050+ while (true)
5151+ {
5252+ rc = dds_read(reader, samples, infos, MAX_SAMPLES, MAX_SAMPLES);
5353+5454+ /* Check if we read some data and it is valid. */
5555+ if ((rc > 0) && (infos[0].valid_data))
5656+ {
5757+ /* Print Message. */
5858+ msg = (LowCmd_*) samples[0];
5959+ printf("=== [Subscriber] Received : ");
6060+ fflush(stdout);
6161+ break;
6262+ }
6363+ else
6464+ {
6565+ dds_sleepfor(DDS_MSECS(20));
6666+ }
6767+ }
6868+```
6969+7070+Enfin, on libère les ressources avant la terminaison du programme
7171+7272+```cpp
7373+ LowCmd__free(samples[0], DDS_FREE_ALL);
7474+ dds_delete(participant);
7575+ return EXIT_SUCCESS;
7676+}
7777+```
7878+7979+Malheureusement, cette solution s'est avérée infructueuse, à cause de (ce qui sera compris bien plus tard) un problème de numéro de domaine DDS.
8080+8181+On change d'approche en préférant plutôt utiliser les abstractions fournies par le SDK de Unitree (cf @receive-lowcmd et @send-lowstate)
8282+8383+8484+Enfin, si un pare-feu est actif, il faut autoriser le traffic udp l'intervalle d'addresses IP `224.0.0.0/4`. Par exemple, avec _ufw_
8585+8686+```bash
8787+sudo ufw allow in proto udp from 224.0.0.0/4
8888+sudo ufw allow in proto udp to 224.0.0.0/4
8989+```
9090+9191+9292+9393+== Installation du plugin dans Gazebo
9494+9595+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`)
9696+9797+#dontbreak(
9898+9999+```cpp
100100+#include <gz/sim/System.hh>
101101+namespace gz_unitree
102102+{
103103+ class UnitreePlugin :
104104+ public gz::sim::System,
105105+ public gz::sim::ISystemPreUpdate
106106+ {
107107+ public:
108108+ UnitreePlugin();
109109+ public:
110110+ ~UnitreePlugin() override;
111111+ public:
112112+ void PreUpdate(const gz::sim::UpdateInfo &_info,
113113+ gz::sim::EntityComponentManager &_ecm) override;
114114+ };
115115+}
116116+```
117117+118118+)
119119+120120+Il faut ensuite implémenter la classe puis appeler une macro ajoutant le plugin à Gazebo
121121+122122+```cpp
123123+#include <gz/plugin/Register.hh>
124124+125125+... // implementation
126126+127127+GZ_ADD_PLUGIN(
128128+ UnitreePlugin,
129129+ gz::sim::System,
130130+ UnitreePlugin::ISystemPreUpdate)
131131+```
132132+133133+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)
134134+135135+#zebraw(
136136+ numbering: false,
137137+ highlight-lines: (..range(3, 5)),
138138+ ```xml
139139+ <sdf version='1.11'>
140140+ <world name="default">
141141+ <plugin filename="gz-unitree" name="gz_unitree::UnitreePlugin">
142142+ </plugin>
143143+ </world>
144144+ <model name='h1_description'>
145145+ <link name='pelvis'>
146146+ <inertial>
147147+ ...
148148+ ```
149149+)
150150+151151+Avec `filename` le chemin vers le plugin compilé, qui sera cherché dans les répertoires spécifiés par `GZ_SIM_SYSTEM_PLUGIN_PATH` @gz-system-plugin-path @sdf-plugin-filename.
152152+153153+154154+== Architecture du plugin
155155+156156+Le plugin consiste en trois parties distinctes:
157157+158158+1. Le "branchement" dans les phases de Gazebo, par l'implémentation de méthodes de `gz::sim::System`
159159+2. L'interaction avec les canaux DDS du SDK d'Unitree
160160+3. Les données et méthodes internes au plugin
161161+162162+En plus de cela, il y a bien évidemment la politique de contrôle $cal(P)$, qui interagit via les canaux DDS avec le robot (qu'il soit réel, ou simulé)
163163+164164+#let legend = (
165165+ ..descriptions
166166+) => grid(
167167+ columns: (1fr, 3fr),
168168+ align: left,
169169+ row-gutter: 0.5em,
170170+ ..descriptions.pos().map(((arrow, desc)) => (
171171+ diagram(edge((0, 0), arrow, (0.75, 0))),
172172+ desc
173173+ )).flatten()
174174+)
175175+176176+#let architecture = (
177177+ caption,
178178+ group-inset: 12pt,
179179+ group-color: luma(80),
180180+ show-legend: true,
181181+ ..edges
182182+) => figure(caption: caption,
183183+ pad(
184184+ y: 10pt + group-inset,
185185+ diagram(
186186+ debug: false,
187187+ node-stroke: 0.5pt,
188188+ edge-corner-radius: 6pt,
189189+ {
190190+191191+ if show-legend {
192192+ node((2, 4.5), stroke: none, width: 15em, legend(("--", "Message DDS"), ("@->", "Désynchronisation")))
193193+ }
194194+195195+ let group = (nodes, label, alignment: bottom + center, name: none) => node(
196196+ name: name,
197197+ enclose: nodes,
198198+ snap: false,
199199+ inset: group-inset,
200200+ stroke: group-color.lighten(75%) + 2pt,
201201+ align(alignment, move(dy: 2 * group-inset * if alignment.y == bottom { 1 } else { -1 }, text(fill: group-color, label)))
202202+ )
203203+204204+ let subtitled = (title, subtitle) => [#title \ #text(size: 0.8em, subtitle)]
205205+206206+ node(name: <configure>, (0, 1), `::Configure`)
207207+ node(name: <preupdate>, (0, 2), `::PreUpdate`)
208208+ group((<configure>, <preupdate>), `gz::sim::System`, alignment: top + center)
209209+210210+ node(name: <channelfactory>, enclose: ((1, 0), (2, 0)), inset: 8pt, subtitled(`ChannelFactory`, [domaine 1, interface `lo`]))
211211+ node(name: <publisher>, (1, 1), inset: 8pt, subtitled(`ChannelPublisher` , [canal `rt/lowstate`]))
212212+ node(name: <subscriber>, (2, 1), inset: 8pt, subtitled(`ChannelSubscriber` , [canal `rt/lowcmd`]))
213213+ group(name: <dds>, (<channelfactory>, <publisher>, <subscriber>), alignment: top+center)[SDK d'Unitree]
214214+215215+216216+ node(name: <lowstate>, (1, 2), `::LowStateWriter`)
217217+ node(name: <lowcmd>, (2, 2), `::CmdHandler`)
218218+ node(name: <statebuf>, (1, 3))[State buffer]
219219+ node(name: <cmdbuf>, (2, 3))[Commands buffer]
220220+ group((<lowstate>, <lowcmd>, <statebuf>, <cmdbuf>))[Plugin internals]
622177-== Émission de l'état
222222+ node(name: <policy>, (0, -1), $cal(P)$)
223223+224224+ for e in edges.pos() {
225225+ e
226226+ }
227227+ }
228228+)))
229229+230230+#architecture([Phase d'initialisation du plugin], show-legend: false, {
231231+ edge(<configure>, "u", <channelfactory>, "->", label-side: left, label-pos: 50%)[appelle]
232232+ edge(<channelfactory>, "->", <publisher>)[initialise]
233233+ edge(<channelfactory>, "->", <subscriber>)[initialise]
234234+ edge(<publisher>, "<->", <lowstate>)[associés]
235235+ edge(<subscriber>, "<->", <lowcmd>)[associés]
236236+})
237237+238238+On commence par instancier un contrôleur dans le domaine DDS n°1, sur l'interface réseau `lo`#footnote[interface dite "loopback", qui est locale à l'ordinateur: ici, le simulateur et la politique de contrôle tournent sur la même machine, donc les messages DDS n'ont pas besoin de "sortir" de celle-ci]
239239+240240+On lui associe:
241241+242242+- Un _publisher_, chargé d'envoyer périodiquement des messages sur `rt/lowstate` en appellant la méthode `LowStateWriter`
243243+- Un _subscriber_, chargé d'appeller la méthode `CmdHandler` avec chaque message arrivant sur `rt/lowcmd`.
244244+245245+Cette initialisation est faite à l'initialisation du plugin par Gazebo, en la faisant dans la méhode `::Configure` du plugin.
246246+247247+== Réception des commandes <receive-lowcmd>
248248+249249+Lorsqu'un message, publié par $cal(P)$ (1A) et contenant des ordres pour les moteurs, arrive sur `rt/lowcmd`, `::CmdHandler` est appelé (2, 3), et modifie un _buffer_ (4) contenant la dernière commande reçue.
250250+251251+252252+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.
253253+254254+#architecture([Phase de réception des commandes], {
255255+ edge(<policy>, (2, -1), (2, 0), "-->", label-pos: 10%)[(1A) publish]
256256+ edge(<policy>, (2, -1), (2, 0), stroke: none, label-pos: 60%, label-side: left)[(1A) subscription]
257257+ edge((2, 0), <subscriber>, "->")[(2)]
258258+ edge(<subscriber>, "->", <lowcmd>)[(3)]
259259+ edge(<lowcmd>, "->", <cmdbuf>)[(4)]
260260+ // edge(<lowcmd.east>, "r,d,d,l,l,l,l,l,l,u,u,u", <preupdate>, "->", label-side: left)[(5)]
261261+ edge(<preupdate>, "d,d,r,r", <cmdbuf>, "<-@")[(1B)]
262262+})
263263+264264+On 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.
265265+266266+267267+/ Si `::PreUpdate` est plus fréquente: Le simulateur appliquera simplement plusieurs fois la même commande, le buffer n'ayant pas été modifié.
268268+269269+/ 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.
270270+271271+// 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`.
272272+//
273273+// #dontbreak[
274274+//
275275+// ```cpp
276276+// ...
277277+//
278278+// void UnitreePlugin::Configure()
279279+// {
280280+// ```
281281+//
282282+// Instanciation d'un canal
283283+//
284284+// ```cpp
285285+// ChannelFactory::Instance()->Init(1, "lo" /* loopback interface */);
286286+// ```
287287+//
288288+// Création de $x |-> mono("CmdHandler")(mono("this"), x)$. L'utilitaire `std::bind` permet de passer à `InitChannel` une fonction simple
289289+//
290290+// ```cpp
291291+// auto handler = std::bind(
292292+// &UnitreePlugin::CmdHandler,
293293+// this,
294294+// std::placeholders::_1
295295+// )
296296+// ```
297297+//
298298+// Création du subscriber
299299+//
300300+// ```cpp
301301+// auto subscriber = ChannelSubscriberPtr<LowCmd_>(
302302+// new ChannelSubscriber<LowCmd_>("rt/lowcmd")
303303+// );
304304+//
305305+//
306306+// subscriber->InitChannel(handler, 1);
307307+// }
308308+// ```
309309+//
310310+// Définition du handler
311311+//
312312+// ```cpp
313313+// void UnitreePlugin::CmdHandler(const void *msg)
314314+// {
315315+// LowCmd_ _cmd = *(const LowCmd_ *)msg;
316316+//
317317+// // Remplissage du buffer interne à la classe
318318+// MotorCommand motor_command_tmp;
319319+// for (size_t i = 0; i < H1_NUM_MOTOR; ++i)
320320+// {
321321+// motor_command_tau_ff[i] = _cmd.motor_cmd()[i].tau();
322322+// ...
323323+// }
324324+//
325325+// this->motor_command_buffer.SetData(motor_command_tmp);
326326+// }
327327+// ```
328328+//
329329+// ]
330330+331331+332332+== Émission de l'état <send-lowstate>
333333+334334+Avant de démarrer un nouveau pas de simulation, la méthode `::PreUpdate` vient mettre à jour l'état du robot simulé en modifiant le _State buffer_ interne au plugin (1A).
335335+336336+Le `LowStateWriter` vient lire le _State buffer_ (1B) pour publier l'état sur le canal DDS (2, 3) qui est ensuite lu par $cal(P)$ (4), qui (on le suppose) poss-de une subscription sur `rt/lowstate`
337337+338338+#let transparent = luma(0).opacify(0%)
339339+340340+341341+#architecture([Phase d'envoi de l'état], {
342342+ edge(<preupdate>, "d,d,r", <statebuf>, "->")[(1A)]
343343+ edge(<statebuf>, "@->", <lowstate>)[(1B)]
344344+ edge(<lowstate>, "->", <publisher>)[(2)]
345345+ edge(<publisher>, "->", (1, 0))[(3)]
346346+ edge(<policy>, (1, -1), (1, 0), "<--", label-pos: 20%)[(4) subscription]
347347+ edge(<policy>, (1, -1), (1, 0), stroke: none, label-pos: 60%, label-side: left)[(4) publish]
348348+})
349349+350350+351351+Ici également, `LowStateWriter` s'exécute _en parallèle_ du code de `::PreUpdate`: En effet, la création du `ChannelPublisher` démarre une boucle qui vient éxécuter `LowStateWriter` périodiquement, dans un autre _thread_: on a donc aucune garantie de synchronisation entre les deux.
352352+353353+Ici, il y a en plus non pas deux, mais _trois_ boucles indépendantes qui sont en jeux:
354354+355355+- La boucle de simulation de Gazebo (fréquence d'appel de `::PreUpdate`),
356356+- La boucle du `ChannelPublisher` (fréquence d'appel de `::LowStateWriter`), et
357357+- La boucle de réception de $cal(P)$ (à quelle fréquence $cal(P)$ est-elle capable de reçevoir des messages)
358358+359359+360360+Similairement à la réception de commandes:
361361+362362+/ Si `::PreUpdate` est plus fréquente: On perdra des états intermédiaires, la résolution temporelle de l'évolution de l'état du robot disponible pour (ou acceptable par#footnote[
363363+ En fonction de si `::LowStateWriter` est plus fréquente que $cal(P)$ (dans ce cas là, c'est ce qui est acceptable par $cal(P)$ qui est limitant) ou inversement (dans ce cas, c'est ce que la boucle du publisher met à disposition de $cal(P)$ qui est limitant)
364364+]) $cal(P)$ sera moins grande
365365+/ Si `::PreUpdate` est moins fréquente: $cal(P)$ reçevra plusieurs fois le même état, ce qui sera représentatif du fait que la simulation n'a pas encore avancé.
366366+367367+== Désynchronisations
368368+369369+Dans un même appel de `::PreUpdate`, on effectue d'abord la mise à jour du _State buffer_, puis on lit dans le _Commands buffer_.
370370+371371+Un cycle correspond donc à trois boucles indépendantes, représentées ci-après:
372372+373373+- Celle de la simulation (en bleu), qui doit englober l'entièreté d'un cycle
374374+- Celle du `ChannelPublisher` (en rouge)
375375+- Celle de $cal(P)$ (en rose)
376376+377377+#architecture([Cycle complet. Un cycle commence avec la flèche "update" partant de `::PreUpdate`], {
378378+ let colored-edge = (color, label, ..args) => edge(stroke: color, label: text(fill: color, label), ..args)
379379+ let sim-edge = (label, ..args) => colored-edge(blue, label, ..args)
380380+ let publisher-edge = (label, ..args) => colored-edge(red, label, ..args)
381381+ let policy-edge = (label, ..args) => colored-edge(fuchsia, label, ..args)
382382+383383+ // Simulation loop
384384+ sim-edge("read", <preupdate>, "d,d,r,r", <cmdbuf>, "<-@")
385385+ sim-edge("update", <preupdate.east>, "d", <statebuf>, "->", label-pos: 70%, label-side: right)
386386+387387+ // lowstate publisher loop
388388+ publisher-edge("read", <statebuf>, "@->", <lowstate>)
389389+ publisher-edge("", <lowstate>, "-", <publisher>)
390390+ publisher-edge("", <publisher>, (1, 0), <channelfactory.west>, "->")
391391+392392+ // policy loop
393393+ // dds part
394394+ policy-edge("commands", <policy>, (2.25, -1), (2.25, 0), <channelfactory.east>, "-->", label-pos: 10%)
395395+ policy-edge("state", <policy>, (0, 0), <channelfactory.west>, "<--@", label-pos: 80%)
396396+ // non-dds part
397397+ policy-edge("", <channelfactory.east>, (2, 0), <subscriber>, "->")
398398+ policy-edge("", <subscriber>, "-", <lowcmd>)
399399+ policy-edge("update", <lowcmd>, "->", <cmdbuf>)
400400+})
401401+402402+Ces désynchronisations pourraient expliquer les problèmes de performance recontrés (cf @perf)
84039404== Essai sur des politiques réelles
104051111-== Amélioration des performances
406406+== Amélioration des performances <perf>
1240713408== Enregistrement de vidéos
14409
···233233234234Le bridge de Mujoco fonctionne en interceptant les messages sur le canal `rt/lowcmd` et en en envoyant dans le canal `rt/lowstate`, qui correspondent respectivement aux commandes envoyées au robot et à l'état (angles des joints, moteurs, valeurs des capteurs, etc) renvoyé par le robot.
235235236236-Le `low` indique que ce sont des messages bas-niveau: par exemple, `rt/lowcmd` correspond directement à des ordres de tension pour les moteurs, et non pas à des messages plus avancés du type "avancer de $x$ mètres" #todo[ces messages plus haut-niveau = sport mode non? dire quand ils servent]
236236+Le `low` indique que ce sont des messages bas-niveau: par exemple, `rt/lowcmd` correspond directement à des ordres de tension pour les moteurs, et non pas à des messages plus avancés tel que "avancer de $x$ mètres" #todo[ces messages plus haut-niveau = sport mode non? dire quand ils servent]
237237238238Les ordres dans `rt/lowcmd` sont ensuite traduits en appels de fonctions de Mujoco pour mettre à jour l'état du robot simulé, et de messages `rt/lowstate` sont créés à partir des données fournies par Mujoco
239239