Logs for my internship @ LAAS-CNRS

Continue rapport

+38
bib.yaml
··· 779 value: https://github.com/unitreerobotics/unitree_mujoco 780 date: '2021-11-01' 781
··· 779 value: https://github.com/unitreerobotics/unitree_mujoco 780 date: '2021-11-01' 781 782 + unitree_ros2: 783 + type: repository 784 + title: unitree_ros2 785 + publisher: GitHub 786 + author: Unitree Robotics 787 + url: 788 + date: '2025-10-22' 789 + value: https://github.com/unitreerobotics/unitree_ros2 790 + date: '2023-10-20' 791 + 792 + cyclonedds-helloworld: 793 + type: web 794 + title: HelloWorld source code — Eclipse Cyclone DDS, 0.11.0 795 + url: 796 + date: '2025-10-22' 797 + value: https://cyclonedds.io/docs/cyclonedds/latest/getting_started/helloworld/helloworld_source_code.html 798 + 799 + sdf-plugin: 800 + type: web 801 + title: SDFormat Specification, world.plugin element 802 + url: 803 + date: '2025-10-22' 804 + value: 'http://sdformat.org/spec?ver=1.12&elem=world#world_plugin' 805 + 806 + sdf-plugin-filename: 807 + type: web 808 + title: SDFormat Specification, world.plugin.filename element 809 + url: 810 + date: '2025-10-22' 811 + value: 'http://sdformat.org/spec?ver=1.12&elem=world#plugin_filename' 812 + 813 + gz-system-plugin-path: 814 + type: web 815 + title: 'Gazebo Sim: Finding resources' 816 + url: 817 + date: '2025-10-22' 818 + value: https://gazebosim.org/api/sim/8/resources.html#:~:text=All%20paths%20on%20the%20GZ_SIM_SYSTEM_PLUGIN_PATH%20environment%20variable 819 +
gepetto.png

This is a binary file and will not be displayed.

+398 -3
rapport/gz-unitree.typ
··· 1 En se basant sur _unitree\_mujoco_, il a donc été possible de réaliser un bridge pour Gazebo. 2 3 == Établissement du contact 4 5 - == Réception des commandes 6 7 - == Émission de l'état 8 9 == Essai sur des politiques réelles 10 11 - == Amélioration des performances 12 13 == Enregistrement de vidéos 14
··· 1 + #import "@preview/zebraw:0.5.5" 2 + #import "@preview/fletcher:0.5.8": diagram, node, edge 3 + #import "./utils.typ": dontbreak 4 + #show figure: set block(spacing: 2em) 5 + #let zebraw = (..args) => zebraw.zebraw(lang: false, background-color: luma(255).opacify(0%), ..args) 6 + 7 En se basant sur _unitree\_mujoco_, il a donc été possible de réaliser un bridge pour Gazebo. 8 9 == Établissement du contact 10 11 + 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 12 + 13 + On commence par importer la bibliothèque DDS et les définitions IDL de `rt/lowcmd` 14 + 15 + ```cpp 16 + #include "messages/LowCmd_.hpp" 17 + #include "dds/dds.h" 18 + ... 19 + 20 + int main (int argc, char ** argv) 21 + { 22 + ``` 23 + 24 + On initialise les différents objets permettant de lire sur un canal 25 + 26 + ```cpp 27 + dds_entity_t participant, topic, reader; 28 + LowCmd_ *msg; 29 + void *samples[MAX_SAMPLES]; 30 + ... 31 + 32 + participant = dds_create_participant(DDS_DOMAIN_DEFAULT, NULL, NULL); 33 + 34 + topic = dds_create_topic(participant, &LowCmd__desc, "HelloWorldData_Msg", NULL, NULL); 35 + 36 + qos = dds_create_qos(); 37 + dds_qset_reliability(qos, DDS_RELIABILITY_RELIABLE, DDS_SECS (10)); 38 + 39 + reader = dds_create_reader(participant, topic, qos, NULL); 40 + 41 + dds_delete_qos(qos); 42 + 43 + samples[0] = LowCmd___alloc(); 44 + ``` 45 + 46 + Et on attend qu'un message arrive sur le canal, pour l'afficher 47 + 48 + ```cpp 49 + /* Poll until data has been read. */ 50 + while (true) 51 + { 52 + rc = dds_read(reader, samples, infos, MAX_SAMPLES, MAX_SAMPLES); 53 + 54 + /* Check if we read some data and it is valid. */ 55 + if ((rc > 0) && (infos[0].valid_data)) 56 + { 57 + /* Print Message. */ 58 + msg = (LowCmd_*) samples[0]; 59 + printf("=== [Subscriber] Received : "); 60 + fflush(stdout); 61 + break; 62 + } 63 + else 64 + { 65 + dds_sleepfor(DDS_MSECS(20)); 66 + } 67 + } 68 + ``` 69 + 70 + Enfin, on libère les ressources avant la terminaison du programme 71 + 72 + ```cpp 73 + LowCmd__free(samples[0], DDS_FREE_ALL); 74 + dds_delete(participant); 75 + return EXIT_SUCCESS; 76 + } 77 + ``` 78 + 79 + 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. 80 + 81 + 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) 82 + 83 + 84 + 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_ 85 + 86 + ```bash 87 + sudo ufw allow in proto udp from 224.0.0.0/4 88 + sudo ufw allow in proto udp to 224.0.0.0/4 89 + ``` 90 + 91 + 92 + 93 + == Installation du plugin dans Gazebo 94 + 95 + 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`) 96 + 97 + #dontbreak( 98 + 99 + ```cpp 100 + #include <gz/sim/System.hh> 101 + namespace gz_unitree 102 + { 103 + class UnitreePlugin : 104 + public gz::sim::System, 105 + public gz::sim::ISystemPreUpdate 106 + { 107 + public: 108 + UnitreePlugin(); 109 + public: 110 + ~UnitreePlugin() override; 111 + public: 112 + void PreUpdate(const gz::sim::UpdateInfo &_info, 113 + gz::sim::EntityComponentManager &_ecm) override; 114 + }; 115 + } 116 + ``` 117 + 118 + ) 119 + 120 + Il faut ensuite implémenter la classe puis appeler une macro ajoutant le plugin à Gazebo 121 + 122 + ```cpp 123 + #include <gz/plugin/Register.hh> 124 + 125 + ... // implementation 126 + 127 + GZ_ADD_PLUGIN( 128 + UnitreePlugin, 129 + gz::sim::System, 130 + UnitreePlugin::ISystemPreUpdate) 131 + ``` 132 + 133 + 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) 134 + 135 + #zebraw( 136 + numbering: false, 137 + highlight-lines: (..range(3, 5)), 138 + ```xml 139 + <sdf version='1.11'> 140 + <world name="default"> 141 + <plugin filename="gz-unitree" name="gz_unitree::UnitreePlugin"> 142 + </plugin> 143 + </world> 144 + <model name='h1_description'> 145 + <link name='pelvis'> 146 + <inertial> 147 + ... 148 + ``` 149 + ) 150 + 151 + 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. 152 + 153 + 154 + == Architecture du plugin 155 + 156 + Le plugin consiste en trois parties distinctes: 157 + 158 + 1. Le "branchement" dans les phases de Gazebo, par l'implémentation de méthodes de `gz::sim::System` 159 + 2. L'interaction avec les canaux DDS du SDK d'Unitree 160 + 3. Les données et méthodes internes au plugin 161 + 162 + 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é) 163 + 164 + #let legend = ( 165 + ..descriptions 166 + ) => grid( 167 + columns: (1fr, 3fr), 168 + align: left, 169 + row-gutter: 0.5em, 170 + ..descriptions.pos().map(((arrow, desc)) => ( 171 + diagram(edge((0, 0), arrow, (0.75, 0))), 172 + desc 173 + )).flatten() 174 + ) 175 + 176 + #let architecture = ( 177 + caption, 178 + group-inset: 12pt, 179 + group-color: luma(80), 180 + show-legend: true, 181 + ..edges 182 + ) => figure(caption: caption, 183 + pad( 184 + y: 10pt + group-inset, 185 + diagram( 186 + debug: false, 187 + node-stroke: 0.5pt, 188 + edge-corner-radius: 6pt, 189 + { 190 + 191 + if show-legend { 192 + node((2, 4.5), stroke: none, width: 15em, legend(("--", "Message DDS"), ("@->", "Désynchronisation"))) 193 + } 194 + 195 + let group = (nodes, label, alignment: bottom + center, name: none) => node( 196 + name: name, 197 + enclose: nodes, 198 + snap: false, 199 + inset: group-inset, 200 + stroke: group-color.lighten(75%) + 2pt, 201 + align(alignment, move(dy: 2 * group-inset * if alignment.y == bottom { 1 } else { -1 }, text(fill: group-color, label))) 202 + ) 203 + 204 + let subtitled = (title, subtitle) => [#title \ #text(size: 0.8em, subtitle)] 205 + 206 + node(name: <configure>, (0, 1), `::Configure`) 207 + node(name: <preupdate>, (0, 2), `::PreUpdate`) 208 + group((<configure>, <preupdate>), `gz::sim::System`, alignment: top + center) 209 + 210 + node(name: <channelfactory>, enclose: ((1, 0), (2, 0)), inset: 8pt, subtitled(`ChannelFactory`, [domaine 1, interface `lo`])) 211 + node(name: <publisher>, (1, 1), inset: 8pt, subtitled(`ChannelPublisher` , [canal `rt/lowstate`])) 212 + node(name: <subscriber>, (2, 1), inset: 8pt, subtitled(`ChannelSubscriber` , [canal `rt/lowcmd`])) 213 + group(name: <dds>, (<channelfactory>, <publisher>, <subscriber>), alignment: top+center)[SDK d'Unitree] 214 + 215 + 216 + node(name: <lowstate>, (1, 2), `::LowStateWriter`) 217 + node(name: <lowcmd>, (2, 2), `::CmdHandler`) 218 + node(name: <statebuf>, (1, 3))[State buffer] 219 + node(name: <cmdbuf>, (2, 3))[Commands buffer] 220 + group((<lowstate>, <lowcmd>, <statebuf>, <cmdbuf>))[Plugin internals] 221 222 + node(name: <policy>, (0, -1), $cal(P)$) 223 + 224 + for e in edges.pos() { 225 + e 226 + } 227 + } 228 + ))) 229 + 230 + #architecture([Phase d'initialisation du plugin], show-legend: false, { 231 + edge(<configure>, "u", <channelfactory>, "->", label-side: left, label-pos: 50%)[appelle] 232 + edge(<channelfactory>, "->", <publisher>)[initialise] 233 + edge(<channelfactory>, "->", <subscriber>)[initialise] 234 + edge(<publisher>, "<->", <lowstate>)[associés] 235 + edge(<subscriber>, "<->", <lowcmd>)[associés] 236 + }) 237 + 238 + 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] 239 + 240 + On lui associe: 241 + 242 + - Un _publisher_, chargé d'envoyer périodiquement des messages sur `rt/lowstate` en appellant la méthode `LowStateWriter` 243 + - Un _subscriber_, chargé d'appeller la méthode `CmdHandler` avec chaque message arrivant sur `rt/lowcmd`. 244 + 245 + Cette initialisation est faite à l'initialisation du plugin par Gazebo, en la faisant dans la méhode `::Configure` du plugin. 246 + 247 + == Réception des commandes <receive-lowcmd> 248 + 249 + 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. 250 + 251 + 252 + 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. 253 + 254 + #architecture([Phase de réception des commandes], { 255 + edge(<policy>, (2, -1), (2, 0), "-->", label-pos: 10%)[(1A) publish] 256 + edge(<policy>, (2, -1), (2, 0), stroke: none, label-pos: 60%, label-side: left)[(1A) subscription] 257 + edge((2, 0), <subscriber>, "->")[(2)] 258 + edge(<subscriber>, "->", <lowcmd>)[(3)] 259 + edge(<lowcmd>, "->", <cmdbuf>)[(4)] 260 + // edge(<lowcmd.east>, "r,d,d,l,l,l,l,l,l,u,u,u", <preupdate>, "->", label-side: left)[(5)] 261 + edge(<preupdate>, "d,d,r,r", <cmdbuf>, "<-@")[(1B)] 262 + }) 263 + 264 + 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. 265 + 266 + 267 + / Si `::PreUpdate` est plus fréquente: Le simulateur appliquera simplement plusieurs fois la même commande, le buffer n'ayant pas été modifié. 268 + 269 + / 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. 270 + 271 + // 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`. 272 + // 273 + // #dontbreak[ 274 + // 275 + // ```cpp 276 + // ... 277 + // 278 + // void UnitreePlugin::Configure() 279 + // { 280 + // ``` 281 + // 282 + // Instanciation d'un canal 283 + // 284 + // ```cpp 285 + // ChannelFactory::Instance()->Init(1, "lo" /* loopback interface */); 286 + // ``` 287 + // 288 + // Création de $x |-> mono("CmdHandler")(mono("this"), x)$. L'utilitaire `std::bind` permet de passer à `InitChannel` une fonction simple 289 + // 290 + // ```cpp 291 + // auto handler = std::bind( 292 + // &UnitreePlugin::CmdHandler, 293 + // this, 294 + // std::placeholders::_1 295 + // ) 296 + // ``` 297 + // 298 + // Création du subscriber 299 + // 300 + // ```cpp 301 + // auto subscriber = ChannelSubscriberPtr<LowCmd_>( 302 + // new ChannelSubscriber<LowCmd_>("rt/lowcmd") 303 + // ); 304 + // 305 + // 306 + // subscriber->InitChannel(handler, 1); 307 + // } 308 + // ``` 309 + // 310 + // Définition du handler 311 + // 312 + // ```cpp 313 + // void UnitreePlugin::CmdHandler(const void *msg) 314 + // { 315 + // LowCmd_ _cmd = *(const LowCmd_ *)msg; 316 + // 317 + // // Remplissage du buffer interne à la classe 318 + // MotorCommand motor_command_tmp; 319 + // for (size_t i = 0; i < H1_NUM_MOTOR; ++i) 320 + // { 321 + // motor_command_tau_ff[i] = _cmd.motor_cmd()[i].tau(); 322 + // ... 323 + // } 324 + // 325 + // this->motor_command_buffer.SetData(motor_command_tmp); 326 + // } 327 + // ``` 328 + // 329 + // ] 330 + 331 + 332 + == Émission de l'état <send-lowstate> 333 + 334 + 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). 335 + 336 + 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` 337 + 338 + #let transparent = luma(0).opacify(0%) 339 + 340 + 341 + #architecture([Phase d'envoi de l'état], { 342 + edge(<preupdate>, "d,d,r", <statebuf>, "->")[(1A)] 343 + edge(<statebuf>, "@->", <lowstate>)[(1B)] 344 + edge(<lowstate>, "->", <publisher>)[(2)] 345 + edge(<publisher>, "->", (1, 0))[(3)] 346 + edge(<policy>, (1, -1), (1, 0), "<--", label-pos: 20%)[(4) subscription] 347 + edge(<policy>, (1, -1), (1, 0), stroke: none, label-pos: 60%, label-side: left)[(4) publish] 348 + }) 349 + 350 + 351 + 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. 352 + 353 + Ici, il y a en plus non pas deux, mais _trois_ boucles indépendantes qui sont en jeux: 354 + 355 + - La boucle de simulation de Gazebo (fréquence d'appel de `::PreUpdate`), 356 + - La boucle du `ChannelPublisher` (fréquence d'appel de `::LowStateWriter`), et 357 + - La boucle de réception de $cal(P)$ (à quelle fréquence $cal(P)$ est-elle capable de reçevoir des messages) 358 + 359 + 360 + Similairement à la réception de commandes: 361 + 362 + / 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[ 363 + 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) 364 + ]) $cal(P)$ sera moins grande 365 + / 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é. 366 + 367 + == Désynchronisations 368 + 369 + Dans un même appel de `::PreUpdate`, on effectue d'abord la mise à jour du _State buffer_, puis on lit dans le _Commands buffer_. 370 + 371 + Un cycle correspond donc à trois boucles indépendantes, représentées ci-après: 372 + 373 + - Celle de la simulation (en bleu), qui doit englober l'entièreté d'un cycle 374 + - Celle du `ChannelPublisher` (en rouge) 375 + - Celle de $cal(P)$ (en rose) 376 + 377 + #architecture([Cycle complet. Un cycle commence avec la flèche "update" partant de `::PreUpdate`], { 378 + let colored-edge = (color, label, ..args) => edge(stroke: color, label: text(fill: color, label), ..args) 379 + let sim-edge = (label, ..args) => colored-edge(blue, label, ..args) 380 + let publisher-edge = (label, ..args) => colored-edge(red, label, ..args) 381 + let policy-edge = (label, ..args) => colored-edge(fuchsia, label, ..args) 382 + 383 + // Simulation loop 384 + sim-edge("read", <preupdate>, "d,d,r,r", <cmdbuf>, "<-@") 385 + sim-edge("update", <preupdate.east>, "d", <statebuf>, "->", label-pos: 70%, label-side: right) 386 + 387 + // lowstate publisher loop 388 + publisher-edge("read", <statebuf>, "@->", <lowstate>) 389 + publisher-edge("", <lowstate>, "-", <publisher>) 390 + publisher-edge("", <publisher>, (1, 0), <channelfactory.west>, "->") 391 + 392 + // policy loop 393 + // dds part 394 + policy-edge("commands", <policy>, (2.25, -1), (2.25, 0), <channelfactory.east>, "-->", label-pos: 10%) 395 + policy-edge("state", <policy>, (0, 0), <channelfactory.west>, "<--@", label-pos: 80%) 396 + // non-dds part 397 + policy-edge("", <channelfactory.east>, (2, 0), <subscriber>, "->") 398 + policy-edge("", <subscriber>, "-", <lowcmd>) 399 + policy-edge("update", <lowcmd>, "->", <cmdbuf>) 400 + }) 401 + 402 + Ces désynchronisations pourraient expliquer les problèmes de performance recontrés (cf @perf) 403 404 == Essai sur des politiques réelles 405 406 + == Amélioration des performances <perf> 407 408 == Enregistrement de vidéos 409
+1
rapport/main.typ
··· 102 #stack( 103 dir: ltr, 104 spacing: 2em, 105 image("../laas.jpeg", height: 10em), 106 image( 107 "../enseeiht.png",
··· 102 #stack( 103 dir: ltr, 104 spacing: 2em, 105 + image("../gepetto.png", height: 10em), 106 image("../laas.jpeg", height: 10em), 107 image( 108 "../enseeiht.png",
+3 -3
rapport/proofs.typ
··· 69 70 $ 71 P(C = (c_t)_(t in NN)) 72 - &= 73 - bb(P)(S_0 = s_0)bb(P)(A_0 = a_0 | S_0 = s_0) dot \ 74 - & product_(t=1)^oo 75 // bb(P)(S_t = s_t mid(|) cases(S_(t-1) = s_(t-1), A_(t-1) = a_(t-1))) 76 bb(P)(S_t = s_t mid(|) C_(t-1) = c_(t-1)) 77 bb(P)(A_t = a_t mid(|) S_t = s_t) \
··· 69 70 $ 71 P(C = (c_t)_(t in NN)) 72 + = \ 73 + bb(P)(S_0 = s_0)bb(P)(A_0 = a_0 | S_0 = s_0) 74 + product_(t=1)^oo 75 // bb(P)(S_t = s_t mid(|) cases(S_(t-1) = s_(t-1), A_(t-1) = a_(t-1))) 76 bb(P)(S_t = s_t mid(|) C_(t-1) = c_(t-1)) 77 bb(P)(A_t = a_t mid(|) S_t = s_t) \
+1 -1
rapport/sdk2-study.typ
··· 233 234 Le 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. 235 236 - 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] 237 238 Les 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 239
··· 233 234 Le 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. 235 236 + 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] 237 238 Les 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 239
+1
rapport/utils.typ
··· 1 #let comment = content => text(fill: gray)[(Note: #content)] 2 #let todo = content => text(fill: red)[(TODO: #content)] 3 #let refneeded = text(fill: luma(100), [[Réf. nécéssaire]]) 4 5 6 #let cut-lines = (
··· 1 #let comment = content => text(fill: gray)[(Note: #content)] 2 #let todo = content => text(fill: red)[(TODO: #content)] 3 #let refneeded = text(fill: luma(100), [[Réf. nécéssaire]]) 4 + #let dontbreak = content => block(breakable: false, content) 5 6 7 #let cut-lines = (