···16161717== Établissement du contact
18181919-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
1919+Une première tentative a été de suivre la documentation de CycloneDDS @cyclonedds-helloworld pour écouter sur le canal `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
20202121-Malheureusement, cette solution s'est avérée infructueuse, à cause d'une inadéquation sur les domaines DDS (ce qui sera compris plus tard).
2121+Malheureusement, cette solution s'est avérée infructueuse, à cause d'une erreur sur les domaines DDS utilisés (ce qui sera compris plus tard).
22222323-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)
2323+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)
24242525-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_
2525+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_ @ufw
26262727```bash
2828sudo ufw allow in proto udp from 224.0.0.0/4
···3434 gutter: 2em,
3535 [
36363737- Pour arriver à ces solutions, du débuggage du traffic RTPS (le protocole sur lequel est construit DDS @dds), _Wireshark_ @wireshark s'est avéré utile.
3737+ Pour arriver à ces solutions, _Wireshark_ @wireshark s'est avéré utile, étant capable d'inspecter du traffic RTPS#footnote[Le protocole sur lequel est construit DDS @dds],
383839394040- C'est notamment grâce à ce traçage des paquets que le problème d'ID de domaine a été découvert: notre _subscriber_ DDS était réglé sur le domaine anonyme (ID 0) alors que le SDK d'Unitree communique sur le domaine d'ID 1.
4040+ C'est notamment grâce à ce traçage des paquets que le problème de domaine DDS a été découvert: notre _subscriber_ DDS était réglé sur le domaine anonyme (ID aléatoire représenté par un 0 lors de la configuration) alors que le SDK d'Unitree communique sur le domaine d'ID 1.
41414242 C'est aussi Wireshark qui nous a permis de voir quels étaient les types IDL utilisés pour les messages.
4343 ],
+19-78
rapport/sdk2-study.typ
···4545 unsigned long reserve[4];
4646 unsigned long crc;
4747 };
4848- ```)
4949-5050-5151-#grid(
5252- columns: 2,
5353- ```python
5454- @dataclass
5555- @annotate.final
5656- @annotate.autoid("sequential")
5757- class MotorCmd_(idl.IdlStruct, typename="MotorCmd"):
5858- mode: types.uint8
5959- q: types.float32
6060- dq: types.float32
6161- tau: types.float32
6262- kp: types.float32
6363- kd: types.float32
6464- reserve: types.uint32
6565-6666- @dataclass
6767- @annotate.final
6868- @annotate.autoid("sequential")
6969- class LowCmd_(idl.IdlStruct, typename="Cmd"):
7070- mode_pr: types.uint8
7171- mode_machine: types.uint8
7272- motor_cmd: types.array['MotorCmd_', 35]
7373- reserve: types.array[types.uint32, 4]
7474- crc: types.uint32
7575-7676- ```,
7777- ```cpp
7878- class LowCmd_
7979- {
8080- private:
8181- uint8_t mode_pr_ = 0;
8282- uint8_t mode_machine_ = 0;
8383- std::array<::MotorCmd_, 35> motor_cmd_ = { };
8484- std::array<uint32_t, 4> reserve_ = { };
8585- uint32_t crc_ = 0;
8686-8787- public:
8888- LowCmd_() = default;
8989-9090- explicit LowCmd_(
9191- uint8_t mode_pr,
9292- uint8_t mode_machine,
9393- const std::array<::MotorCmd_, 35>& motor_cmd,
9494- const std::array<uint32_t, 4>& reserve,
9595- uint32_t crc) :
9696- mode_pr_(mode_pr),
9797- mode_machine_(mode_machine),
9898- motor_cmd_(motor_cmd),
9999- reserve_(reserve),
100100- crc_(crc) { }
101101-102102- uint8_t mode_pr() const { return this->mode_pr_; }
103103- uint8_t& mode_pr() { return this->mode_pr_; }
104104- void mode_pr(uint8_t _val_) { this->mode_pr_ = _val_; }
105105- uint8_t mode_machine() const { return this->mode_machine_; }
10648 ```
107107- )
108108-))
4949+)
10950110110-DDS groupe les mesages dans des _topics_. Les messages sont échangés sur un topic de la manière suivante
5151+DDS groupe les messages dans des _topics_. Les messages sont échangés sur un topic de la manière suivante
1115211253/ Lecture: En s'abonnant au topic, on reçoit en temps réel les messages qui sont envoyés dessus
11354/ Écriture: En publiant des messages sur le topic, on les rend disponibles aux abonnés
1145511556#import "@preview/unify:0.7.1": qty
11657117117-CycloneDDS est capable d'un débit d'environ #qty("1", "GB/s"), pour des messages d'environ #qty("1", "kB") chacun @dds-benchmark. On remarque, en pratique, des messages entre #qty("0.9", "kB") et #qty("1.3", "kB") dans le cas des échanges commandes/état avec le robot
5858+CycloneDDS est capable d'un débit d'environ #qty("1", "GB/s"), pour des messages d'environ #qty("1", "kB") chacun @dds-benchmark. On remarque, en pratique, des tailles de message entre #qty("0.9", "kB") et #qty("1.3", "kB") dans le cas des échanges commandes/état avec le robot.
11859119119-Et enfin, les _topics_ peuvent être isolés d'autres topics via des _domain_#[s].
6060+Et enfin, les topics peuvent être isolés d'autres topics via des _domain_#[s], identifiés par un numéro. Deux topics portant le même nom reste isolés si ils sont sur deux domaines différents.
120611216212263== Une base de code partiellement open-source
12364124124-Le code source du SDK d'unitree est disponible sur Github @sdk2_source_today. Cependant, le dépôt git comprend des fichiers binaires déjà compilés:
6565+Le code source du SDK d'Unitree est disponible sur Github @sdk2_source_today. Cependant, le dépôt git comprend des fichiers binaires déjà compilés:
1256612667#figure(
127127- caption: [Résultat de `tree lib thirdparty` dans le dépot git],
6868+ caption: [Résultat de `tree lib/ thirdparty/` dans le dépot git],
12869 ```
12970 lib
13071 ├── aarch64
···159100)
160101161102#figure(
162162- caption: [Extrait de `cmake/unitree_sdk2Targets.cmake`],
103103+ caption: [Extrait de `cmake/unitree_sdk2Targets.cmake` @unitree_sdk2],
163104 kind: raw,
164105 zebraw(
165106 numbering-offset: 63 - 1,
···176117 ),
177118)
178119179179-Ici est défini, via `set_target_properties(... IMPORTED_LOCATION)`, le chemin d'une bibliothèque à lier avec la bibliothèque finale @cmake-imported-location.
120120+Ici est défini, via `set_target_properties(... IMPORTED_LOCATION)`, le chemin d'une bibliothèque à lier avec la bibliothèque finale @cmake-imported-location. Ici, c'est un des fichiers pré-compilés que l'on lie.
180121181181-On confirme ceci en lançant `mkdir build && cd build && cmake ..` après avoir supprimé le répertoire `lib/` :
122122+On confirme cette nécéssite en lançant `mkdir build && cd build && cmake ..` après avoir supprimé le répertoire `lib/` :
182123183124#{
184125 show regex(".*CMake Error.*"): set text(fill: red)
···234175235176Ces particularités laissent planner quelques doutes sur la nature open-source du code: ces binaires requis sont-ils seulement présent pour améliorer l'expérience développeur en accélererant la compilation, ou "cachent"-ils du code non public?
236177237237-Ces constats ont motivé une première tentative de décompilation de ces `libunitree_sdk2.a` pour comprendre le fonctionnement du SDK2, via _Ghidra_ @ghidra.
178178+Ces constats ont motivé une première tentative de décompilation de ces `libunitree_sdk2.a` pour comprendre le fonctionnement du SDK, via _Ghidra_ @ghidra.
238179239239-Cependant, la découverte de l'existance d'un bridge officiel SDK $arrows.lr$ Mujoco @unitree_mujoco a rendu cette piste non nécéssaire.
180180+Cependant, la découverte de l'existance d'un bridge officiel SDK $arrows.lr$ Mujoco @unitree_mujoco a rendu l'exploration de cette piste non nécéssaire.
240181241182== Un autre bridge existant: `unitree_mujoco`
242183···247188#figure(caption: "Fonctionnement usuel du SDK", diagram({
248189 node((0, 0), $Pi$)
249190 node((1, 0), "SDK")
250250- node((2, 0), "Robot")
191191+ node((2, 0), "robot")
251192252193 edge((0, 0), (0, 0), "<-", bend: 130deg, loop-angle: 180deg)[]
253194 edge((2.25, 0), (2.25, 0), "->", bend: -130deg, loop-angle: -180deg)[]
···259200 }
260201}))
261202262262-Un bridge se substitue au Robot physique, interceptant les ordres du SDK et les traduisants en des appels de fonctions utilisant l'API du simulateur, et symmétriquement pour les envois d'états au SDK. On peut apparenter le fonctionnement d'un bridge à celui d'une attaque informatique de type "Man in the Middle" (MitM).
203203+Un bridge se substitue au robot physique, interceptant les ordres du SDK et les traduisants en des appels de fonctions provenant de l'API du simulateur, et symmétriquement pour les envois d'états au SDK. On peut apparenter le fonctionnement d'un bridge à celui d'une attaque informatique de type "Man in the Middle" (MitM).
263204264205265206#figure(caption: [Fonctionnement via _unitree\_mujoco_ du SDK], diagram({
···323264 )[*API de Gazebo*])
324265}))
325266326326-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.
267267+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) reçu depuis le robot.
327268328328-Le `low` indique que ce sont des messages bas-niveau. Par exemple, `rt/lowcmd` correspond directement à des ordres de tension pour les moteurs, au lieu d'envoyer des ordres plus évolués, tels que "se déplacer de $x$ mètres en avant" @h1-motion-services
269269+Le `low` indique que ce sont des messages bas-niveau. Par exemple, `rt/lowcmd` correspond directement à des ordres en valeurs de couple pour les moteurs, au lieu d'envoyer des ordres plus évolués, tels que "se déplacer de $x$ mètres en avant" @h1-motion-services
329270330330-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
271271+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.
331272332332-Étant donné le modèle _pub/sub_ de DDS, on parle de _pub(lication)_ de message, et de _sub(scription)_#footnote[abonnement] aux messages d'un canal (pour les recevoir)
273273+Étant donné le modèle _pub/sub_ de DDS, on parle de _pub(lication)_ de message, et de _sub(scription)_#footnote[abonnement] aux messages d'un canal (pour les recevoir).
333274334275#figure(
335276 caption: [Cycle de vie de la simulation avec le bridge pour Mujoco],
···354295355296356297 edge(<sdk>, <lowcmd>, "->", bend: 30deg)[pub]
357357- edge(<lowcmd>, (1, 2), "..>", bend: 20deg)[via sub]
298298+ edge(<lowcmd>, (1, 2), "-->", bend: 20deg)[via sub]
358299 edge((1, 2), <mujoco>, "->", bend: 20deg, `data->ctrl[i] = ...`)
359300360360- edge(<sdk>, <lowstate>, "<..", bend: -30deg)[via sub]
301301+ edge(<sdk>, <lowstate>, "<--", bend: -30deg)[via sub]
361302 edge(<lowstate>, (-1, 2), "<-", bend: -20deg)[pub]
362303 edge((-1, 2), <mujoco>, "<-", bend: -20deg, `... = data->sensordata[i]`)
363304