LeRobot documentation

Apportez votre propre matériel

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Apportez votre propre matériel

Ce tutoriel explique comment intégrer votre propre conception de robot dans l'écosystème LeRobot et lui donner accès à l'ensemble de nos outils (collecte de données, pipelines de contrôle, entraînement et inférence de politiques).

À cette fin, nous fournissons la classe de base Robot dans LeRobot, qui spécifie une interface standard pour l'intégration de robots physiques. Voyons comment l'implémenter.

Prérequis

  • Votre propre robot qui expose une interface de communication (par exemple, série, CAN, TCP)
  • Un moyen de lire les données des capteurs et d'envoyer des commandes aux moteurs de manière programmatique, par exemple le SDK ou l'API du fabricant, ou votre propre implémentation de protocole.
  • LeRobot installé dans votre environnement. Suivez notre Guide d’installation.

Choisissez vos moteurs

Si vous utilisez des moteurs Feetech ou Dynamixel, LeRobot fournit une interface bus intégrée, SerialMotorsBus, qui pilote l’une ou l’autre famille : le modèle de chaque moteur lui indique laquelle. Consultez-la pour en apprendre plus sur son API. Elle nécessite l’extra serial-motors, qui installe rustypot : pip install 'lerobot[serial-motors]', ou pip install -e ".[serial-motors]" depuis un checkout source. Pour un bon exemple de son utilisation, vous pouvez consulter notre propre implémentation du suiveur SO101

Utilisez-les s'ils sont compatibles. Sinon, vous devrez trouver ou écrire une interface Python (non abordé dans ce tutoriel) :

  • Trouver un SDK existant en Python (ou utiliser des bindings vers C/C++)
  • Ou implémenter un wrapper de communication de base (par exemple, via pyserial, socket ou CANopen)

Vous n’êtes pas seul—de nombreuses contributions de la communauté utilisent des cartes ou des firmwares personnalisés !

Pour Feetech et Dynamixel, nous prenons actuellement en charge ces servomoteurs :

  • Séries STS et SMS de Feetech : sts3215, sts3250, sm8512bl
  • Série SCS de Feetech : scs0009
  • Série Dynamixel X (protocole 2.0) : xl330-m077, xl330-m288, xc330-t181, xc330-t288, xl430-w250, xl430-w250-2, xc430-w150, xm430-w350, xm540-w270, xh540-w150

Les modèles peuvent partager un bus, des servomoteurs STS/SMS et SCS ou des XL430 et XL330 : chaque moteur est lu et écrit via la définition de son propre modèle, ordre des octets et encodage du signe compris. Les différences entre les familles proviennent des mêmes définitions : quelle valeur de mode de fonctionnement correspond au contrôle de position (bus.set_operating_mode("position")), si le décalage de référence s’ajoute à la position ou s’en soustrait, quels registres existent.

Prendre en charge un modèle ne signifie pas que tous les utilitaires de calibration fonctionnent dessus : le Feetech SCS0009 ne possède ni mode de fonctionnement ni décalage de position d’origine, de sorte que set_operating_mode() et set_half_turn_homings() le refusent. La recette de calibration ci-dessous est destinée aux servomoteurs qui disposent des deux, comme le STS ; un servomoteur SCS est calibré par sa plage seule.

Si vous utilisez des servomoteurs Feetech ou Dynamixel qui ne figurent pas dans cette liste, ajoutez-les à rustypot, qui contient les tables de contrôle lues par les bus : un modèle partageant une table de contrôle existante n’a besoin que de l’enregistrement de son numéro de modèle, tandis qu’un nouveau modèle nécessite une définition de servomoteur (voir le guide de contribution pour les servomoteurs). LeRobot lit les modèles depuis le paquet Python rustypot installé, il doit donc inclure le nouveau modèle : soit une version de rustypot qui l’inclut, soit rustypot compilé à partir du code source.

Dans les sections suivantes, les exemples utilisent des moteurs Feetech. Modifiez les modèles et adaptez-les à vos moteurs si nécessaire.

Étape 1 : Sous-classer l'interface Robot

Vous devez d’abord spécifier la classe de configuration et un identifiant sous forme de chaîne (name) pour votre robot. Si votre robot a des besoins spécifiques que vous souhaitez pouvoir modifier facilement, ils doivent être définis ici (par exemple, port/adresse, baudrate).

Ici, nous ajouterons le nom du port et une caméra par défaut pour notre robot :

from dataclasses import dataclass, field

from lerobot.cameras import CameraConfig
from lerobot.cameras.opencv import OpenCVCameraConfig
from lerobot.robots import RobotConfig


@RobotConfig.register_subclass("my_cool_robot")
@dataclass
class MyCoolRobotConfig(RobotConfig):
    port: str
    cameras: dict[str, CameraConfig] = field(
        default_factory=lambda: {
            "cam_1": OpenCVCameraConfig(
                index_or_path=2,
                fps=30,
                width=480,
                height=640,
            ),
        }
    )

Tutoriel sur les caméras pour comprendre comment détecter et ajouter votre caméra.

Ensuite, nous allons créer notre classe de robot proprement dite, qui hérite de Robot. Cette classe abstraite définit un contrat que vous devez respecter pour que votre robot soit utilisable avec le reste des outils LeRobot.

Ici, nous allons créer un robot simple à 5 degrés de liberté avec une caméra. Il pourrait s’agir d’un simple bras, mais notez que la classe abstraite Robot ne fait aucune hypothèse sur le facteur de forme de votre robot. Laissez libre cours à votre imagination lors de la conception de nouveaux robots !

from typing import Any

from lerobot.cameras import make_cameras_from_configs
from lerobot.motors import Motor, MotorCalibration, MotorNormMode, SerialMotorsBus
from lerobot.robots import Robot

class MyCoolRobot(Robot):
    config_class = MyCoolRobotConfig
    name = "my_cool_robot"

    def __init__(self, config: MyCoolRobotConfig):
        super().__init__(config)
        self.config = config
        self.bus = SerialMotorsBus(
            port=self.config.port,
            motors={
                "joint_1": Motor(1, "sts3250", MotorNormMode.RANGE_M100_100),
                "joint_2": Motor(2, "sts3215", MotorNormMode.RANGE_M100_100),
                "joint_3": Motor(3, "sts3215", MotorNormMode.RANGE_M100_100),
                "joint_4": Motor(4, "sts3215", MotorNormMode.RANGE_M100_100),
                "joint_5": Motor(5, "sts3215", MotorNormMode.RANGE_M100_100),
            },
            calibration=self.calibration,
        )
        self.cameras = make_cameras_from_configs(config.cameras)

Étape 2 : Définir les fonctionnalités d'observation et d'action

Ces deux propriétés définissent le contrat d'interface entre votre robot et les outils qui l'utilisent (tels que les pipelines de collecte de données ou d'apprentissage).

Notez que ces propriétés doivent être appelables même si le robot n'est pas encore connecté, il est donc préférable d'éviter de s'appuyer sur l'état matériel d'exécution pour les définir.

observation_features

Cette propriété doit retourner un dictionnaire décrivant la structure des sorties des capteurs de votre robot. Les clés correspondent à ce que renvoie get_observation(), et les valeurs décrivent soit la forme (pour les tableaux/images), soit le type (pour les valeurs simples).

Exemple pour notre bras 5-DoF avec une caméra :

@property
def _motors_ft(self) -> dict[str, type]:
    return {
        "joint_1.pos": float,
        "joint_2.pos": float,
        "joint_3.pos": float,
        "joint_4.pos": float,
        "joint_5.pos": float,
    }

@property
def _cameras_ft(self) -> dict[str, tuple]:
    return {
        cam: (self.cameras[cam].height, self.cameras[cam].width, 3) for cam in self.cameras
    }

@property
def observation_features(self) -> dict:
    return {**self._motors_ft, **self._cameras_ft}

Dans ce cas, les observations consistent en un simple dict stockant la position de chaque moteur et une image de caméra.

action_features

Cette propriété décrit les commandes que votre robot attend via send_action(). Là encore, les clés doivent correspondre au format d'entrée attendu, et les valeurs définissent la forme/le type de chaque commande.

Ici, nous utilisons simplement les mêmes caractéristiques proprioceptives des articulations (self._motors_ft) que pour observation_features : l’action envoyée sera simplement la position cible pour chaque moteur.

@property
def action_features(self) -> dict:
    return self._motors_ft

Étape 3 : Gérer la connexion et la déconnexion

Ces méthodes doivent gérer l'ouverture et la fermeture de la communication avec votre matériel (par exemple, les ports série, les interfaces CAN, les périphériques USB, les caméras).

is_connected

Cette propriété doit simplement refléter que la communication avec le matériel du robot est établie. Lorsque cette propriété est True, il doit être possible de lire et d'écrire sur le matériel à l'aide de get_observation() et send_action().

@property
def is_connected(self) -> bool:
    return self.bus.is_connected and all(cam.is_connected for cam in self.cameras.values())

connect()

Cette méthode doit établir la communication avec le matériel. De plus, si votre robot nécessite une calibration et qu'il n'est pas calibré, il doit lancer une procédure de calibration par défaut. Si votre robot nécessite une configuration spécifique, celle-ci doit également être appelée ici.

def connect(self, calibrate: bool = True) -> None:
    self.bus.connect()
    if not self.is_calibrated and calibrate:
        self.calibrate()

    for cam in self.cameras.values():
        cam.connect()

    self.configure()

disconnect()

Cette méthode doit mettre fin proprement à la communication avec le matériel : libérer les ressources associées (threads ou processus), fermer les ports, etc.

Ici, nous gérons déjà cela dans nos classes SerialMotorsBus et Camera, il nous suffit donc d'appeler leurs méthodes disconnect() respectives :

def disconnect(self) -> None:
    self.bus.disconnect()
    for cam in self.cameras.values():
        cam.disconnect()

Étape 4 : Prise en charge de l'étalonnage et de la configuration

LeRobot prend en charge l'enregistrement et le chargement automatiques des données d'étalonnage. Cela est utile pour les décalages d'articulations, les positions zéro ou l'alignement des capteurs.

Notez que, selon votre matériel, cela peut ne pas s'appliquer. Dans ce cas, vous pouvez simplement laisser ces méthodes en tant que no-ops :

@property
def is_calibrated(self) -> bool:
    return True

def calibrate(self) -> None:
    pass

is_calibrated

Cela doit refléter si votre robot a bien chargé l'étalonnage requis.

@property
def is_calibrated(self) -> bool:
    return self.bus.is_calibrated

calibrate()

L'objectif de la calibration est double :

  • Connaître la plage de mouvement physique de chaque moteur afin de n'envoyer des commandes que dans cette plage.
  • Normaliser les positions brutes des moteurs en valeurs continues cohérentes (par exemple, des pourcentages, des degrés) plutôt qu'en valeurs discrètes arbitraires dépendant du moteur spécifique utilisé, qui ne seraient pas reproductibles ailleurs.

Il doit implémenter la logique de calibration (le cas échéant) et mettre à jour le dictionnaire self.calibration. Si vous utilisez des moteurs Feetech ou Dynamixel, SerialMotorsBus effectue déjà la calibration manuellement :

  1. _keep_calibration_file() propose de réutiliser le fichier de calibration de cet identifiant. Si vous le conservez, il est écrit sur les moteurs et la calibration s'arrête là.
  2. record_calibration() libère les moteurs et les place dans le mode de fonctionnement que vous spécifiez.
  3. Il vous demande de maintenir les articulations au milieu de leur amplitude de mouvement : cette pose correspond à un demi-tour de chaque moteur.
  4. Elle enregistre la plage de chaque articulation au fur et à mesure que vous la déplacez, jusqu'à ce que vous appuyiez sur ENTER. Les articulations qui tournent en continu sont placées dans full_turn_motors et reçoivent la plage complète de l'encodeur à la place ; notre bras 5-DoF n'en a aucun.
  5. write_calibration() écrit le résultat sur les moteurs, et _save_calibration() le sauvegarde dans le fichier de calibration.
def calibrate(self) -> None:
    if self._keep_calibration_file(self.bus):
        return

    self.calibration = self.bus.record_calibration("position", full_turn_motors=[])
    self.bus.write_calibration(self.calibration)
    self._save_calibration()
    print("Calibration saved to", self.calibration_fpath)

configure()

Utilisez ceci pour configurer tout paramètre de votre matériel (modes de contrôle des servomoteurs, gains du contrôleur, etc.). Cette opération doit généralement être exécutée lors de la connexion et être idempotente.

def configure(self) -> None:
    with self.bus.torque_disabled():
        self.bus.configure_motors()
        for motor in self.bus.motors:
            self.bus.set_operating_mode("position", motor)
            self.bus.write("P_Coefficient", motor, 16)
            self.bus.write("I_Coefficient", motor, 0)
            self.bus.write("D_Coefficient", motor, 32)

Étape 5 : Implémenter la lecture des capteurs et l’envoi des actions

Ce sont les fonctions d'exécution les plus importantes : la boucle d'entrée/sortie principale.

get_observation()

Retourne un dictionnaire de valeurs de capteurs du robot. Ces valeurs incluent généralement les états des moteurs, les images de la caméra, divers capteurs, etc. Dans le framework LeRobot, ces observations sont celles qui seront transmises à une politique afin de prédire les actions à effectuer. Les clés et la structure du dictionnaire doivent correspondre à observation_features.

def get_observation(self) -> dict[str, Any]:
    if not self.is_connected:
        raise ConnectionError(f"{self} is not connected.")

    # Read arm position
    obs_dict = self.bus.sync_read("Present_Position")
    obs_dict = {f"{motor}.pos": val for motor, val in obs_dict.items()}

    # Capture images from cameras
    for cam_key, cam in self.cameras.items():
        obs_dict[cam_key] = cam.async_read()

    return obs_dict

send_action()

Prend un dictionnaire correspondant à action_features et l'envoie à votre matériel. Vous pouvez ajouter des limites de sécurité (clipping, lissage) et renvoyer ce qui a réellement été envoyé.

Pour simplifier, nous n'ajouterons aucune modification des actions dans notre exemple ici.

def send_action(self, action: dict[str, Any]) -> dict[str, Any]:
    goal_pos = {key.removesuffix(".pos"): val for key, val in action.items()}

    # Send goal position to the arm
    self.bus.sync_write("Goal_Position", goal_pos)

    return action

Attente entre les prédictions

Le RTC local ordinaire reste utilisable sans nouvelle capacité robotique. Le déploiement à distance, les pauses asynchrones planifiées du langage et la famine d’actions récupérable exigent un contrat d’attente de cible conservée. Optez pour supports_position_hold = True uniquement si le rafraîchissement de chaque champ d’action via send_action() est sûr pour le contrôleur. Habituellement, ces champs sont des cibles de position réelles. Un contrôleur peut au contraire opter pour une commande conservée validée séparément, comme le jeton de mouvement latent de SONIC, si sa répétition permet au contrôleur embarqué de maintenir l’équilibre. L’activation du couple ou un suffixe .pos seul n’établit pas cela ; les commandes de vitesse/effort et de locomotion restent inadaptées.

Un robot capable d’attendre sans commande de réinitialisation, tel qu’un simulateur qui ne progresse que lors de l’appel de send_action(), définit supports_command_hold = True et implémente hold(). Il est utilisé à la place du maintien de position.

L’implémentation partagée du déroulement actualise la dernière action finie complète renvoyée par send_action(), y compris le pince et tout rognage du pilote. Renvoyez la cible réellement appliquée, et non la demande d’origine, lorsque votre pilote la modifie. Les mises à jour d’observation ultérieures ne remplacent pas cette cible. Avant qu’une commande n’ait été appliquée, un instantané de position mesurée et validée l’initialise. Le robot peut se stabiliser vers sa cible conservée ; ce contrat arrête l’avancement de la trajectoire plutôt que de garantir un arrêt physique instantané. Validez le support de charge, la prise et le comportement d’actualisation des commandes sur le matériel pour chaque déploiement.

Lorsque la fonction de maintien est activée, les processeurs d’actions personnalisés et les corrections de téléopération doivent conserver une commande de position complète, y compris celle de la pince. Le pilote doit renvoyer chaque composant d’action avec des valeurs appliquées finies. Un retour incomplet ou invalide constitue une violation du contrat après l’application de la commande : le déroulement s’arrête et supprime le retour à la position d’origine au lieu de réutiliser une cible antérieure. Les échecs de commande suppriment également le retour à la position d’origine lors d’un déroulement synchrone.

Remote rollout vérifie automatiquement cette capacité du pilote ; aucun drapeau CLI supplémentaire de mode de maintien n’est nécessaire. Les échecs d’observation génériques utilisent le chemin de nettoyage de rollout normal. Le retour à la position initiale nécessite toujours une observation complète et peut échouer si une caméra reste indisponible.

Implémentation du robotPrise en charge de l'attente position-cible
Abonnés SO (SO-100/SO-101), OMX, KochPrise en charge.
HopeJr bras et mainPrise en charge.
Rebot B601Prise en charge dans les modes de bras/pince à position absolue existants.
YAMPrise en charge. Si aucune commande n'arrive pendant command_timeout_s, le bras maintient sa position ou flotte, selon la valeur de idle_mode.
OpenArmPrise en charge avec use_velocity_and_torque=false ; les schémas d'action mixtes sont exclus.
BiSO, BiOpenArm, BiRebot B601, BiYAMPris en charge lorsque les deux bras sous-jacents le sont ; leurs cibles appliquées conservent les préfixes gauche/droite.
Reachy2Prise en charge uniquement avec with_mobile_base=false, use_external_commands=false et max_relative_target=null. Les commandes mobile-base sont des vitesses, le mode externe ne transmet pas de cibles, et le chemin de troncature renvoie actuellement les valeurs d'origine plutôt que les valeurs appliquées.
LeKiwi (y compris le client réseau), EarthRover Mini PlusNon pris en charge : les actions incluent des vitesses de base mobiles, qui ne peuvent pas être conservées comme cibles de position.
Unitree G1Prise en charge uniquement avec SonicWholeBodyController : l’attente répète le dernier jeton de mouvement 64-D complet pendant que le décodeur embarqué continue d’assurer l’équilibre. La locomotion GR00T/Holosoma et le contrôle brut .q restent non pris en charge.

Ces déclarations reflètent le contrat de commande du pilote, et non la validation physique de chaque incarnation ou configuration de contrôleur. La valeur par défaut générique de Robot reste fausse. Les vérifications de capacité n’établissent pas la compatibilité des points de contrôle, le réglage des contrôleurs, la prise en charge de la charge ou l’état de santé du matériel.

Ajouter un téléopérateur

Pour la mise en œuvre de dispositifs de téléopération, nous fournissons également une classe de base Teleoperator. Cette classe est très similaire à la classe de base Robot et ne fait aucune hypothèse sur le facteur de forme.

Les principales différences se trouvent dans les fonctions d’entrée/sortie : un téléopérateur permet de produire une action via get_action et peut recevoir des actions de retour d’information via send_feedback. Le retour d’information peut être tout élément contrôlable sur l’appareil de téléopération qui aide la personne qui le manipule à comprendre les conséquences des actions envoyées. Pensez par exemple au retour de mouvement/force sur un bras maître, ou aux vibrations sur une manette de jeu. Pour implémenter un téléopérateur, vous pouvez suivre ce même tutoriel et l’adapter à ces deux méthodes.

Utiliser vos propres appareils LeRobot 🔌

Vous pouvez facilement étendre lerobot avec votre propre matériel personnalisé—qu’il s’agisse d’une caméra, d’un robot ou d’un dispositif de téléopération—en créant un paquet Python distinct et installable. Si vous respectez quelques conventions simples, les outils en ligne de commande de lerobot (comme lerobot-teleoperate et lerobot-record) découvriront et intégreront automatiquement vos créations sans nécessiter de modification du code source de lerobot.

Ce guide détaille les conventions que votre plugin doit respecter.

Les 4 conventions fondamentales

Pour vous assurer que votre appareil personnalisé est détectable, vous devez respecter les quatre règles suivantes.

1 . Créer un package installable avec un préfixe spécifique

Votre projet doit être un paquet Python standard et installable. Il est essentiel que le nom de votre paquet (tel que défini dans pyproject.toml ou setup.py) commence par l'un de ces préfixes :

  • lerobot_robot_ pour un robot.
  • lerobot_camera_ pour une caméra.
  • lerobot_teleoperator_ pour un dispositif de téléopération.
  • lerobot_strategy_ pour une stratégie lerobot-rollout (voir Ajouter votre propre stratégie).

Ce système de préfixe est la méthode par laquelle lerobot trouve automatiquement votre plugin dans l’environnement Python.

2 . Suivre le modèle de nommage SomethingConfig / Something

La classe d’implémentation de votre appareil doit porter le nom de sa classe de configuration, en supprimant simplement le suffixe Config.

  • Classe de configuration : MyAwesomeTeleopConfig
  • Classe d'appareil : MyAwesomeTeleop

3. Placez vos fichiers dans une structure prévisible

La classe de périphérique (MyAwesomeTeleop) doit se trouver dans un module prévisible par rapport à sa classe de configuration (MyAwesomeTeleopConfig). lerobot recherche automatiquement dans ces emplacements :

  • Dans le même module que la classe de configuration.
  • Dans un sous-module nommé d'après l'appareil (par exemple, my_awesome_teleop.py).

La structure recommandée et la plus simple consiste à les placer dans des fichiers distincts, clairement nommés, au sein du même répertoire.

4 . Exposer les classes dans __init__.py

Le fichier __init__.py de votre package doit importer et exposer à la fois les classes de configuration et de périphérique, afin de les rendre facilement accessibles.

Tout mettre ensemble : un exemple complet

Créons un nouveau téléopérateur appelé my_awesome_teleop.

Structure du répertoire

Voici à quoi devrait ressembler le dossier du projet. Le nom du package, lerobot_teleoperator_my_awesome_teleop, suit la Convention n°1.

lerobot_teleoperator_my_awesome_teleop/
├── pyproject.toml # (or setup.py) lists lerobot as a dependency
└── lerobot_teleoperator_my_awesome_teleop/
    ├── __init__.py
    ├── config_my_awesome_teleop.py
    └── my_awesome_teleop.py

Contenu du fichier

  • config_my_awesome_teleop.py : définit la classe de configuration. Notez le suffixe Config (Convention n°2).

    from dataclasses import dataclass
    
    from lerobot.teleoperators.config import TeleoperatorConfig
    
    @TeleoperatorConfig.register_subclass("my_awesome_teleop")
    @dataclass
    class MyAwesomeTeleopConfig(TeleoperatorConfig):
        # Your configuration fields go here
        port: str = "192.168.1.1"
  • my_awesome_teleop.py : implémente l’appareil. Le nom de la classe MyAwesomeTeleop correspond au nom de sa classe de configuration (Convention n°2). Cette structure de fichier respecte la Convention n°3.

    from lerobot.teleoperators.teleoperator import Teleoperator
    
    from .config_my_awesome_teleop import MyAwesomeTeleopConfig
    
    class MyAwesomeTeleop(Teleoperator):
        config_class = MyAwesomeTeleopConfig
        name = "my_awesome_teleop"
    
        def __init__(self, config: MyAwesomeTeleopConfig):
            super().__init__(config)
            self.config = config
    
        # Your device logic (e.g., connect) goes here
  • __init__.py : Expose les classes principales (Convention n°4).

    from .config_my_awesome_teleop import MyAwesomeTeleopConfig
    from .my_awesome_teleop import MyAwesomeTeleop

Installation et utilisation

  1. Installez votre nouveau plugin dans votre environnement Python. Vous pouvez installer votre package de plugin local en utilisant le mode éditable de pip ou depuis PyPi.

    # Locally
    # Navigate to your plugin's root directory and install it
    cd lerobot_teleoperator_my_awesome_teleop
    pip install -e .
    
    # From PyPi
    pip install lerobot_teleoperator_my_awesome_teleop
  2. Utilisez-le directement depuis la ligne de commande. Vous pouvez désormais utiliser votre appareil personnalisé en faisant référence à son type.

    # Add the other arguments you need (robot type, ports, ids, ...)
    lerobot-teleoperate --teleop.type=my_awesome_teleop ...

Et c’est tout ! Votre appareil personnalisé est désormais pleinement intégré.

Vous cherchez un exemple ?

La communauté maintient des dizaines de plugins construits de cette manière. Parcourez Third-Party Robots & Teleoperators et Third-Party Cameras & Sensors pour en trouver un proche de votre matériel, ou commencez par ces deux petits packages :

Pour conclure

Une fois votre classe de robot terminée, vous pouvez tirer parti de l'écosystème LeRobot :

  • Pilotez votre robot avec les téléopérateurs disponibles ou intégrez directement votre dispositif de téléopération
  • Enregistrer les données d'entraînement et les visualiser
  • L'intégrer dans des pipelines d'apprentissage par renforcement ou d'apprentissage par imitation

Une fois votre plugin publié, ouvrez une PR pour l’ajouter à Third-Party Robots & Teleoperators ou Third-Party Cameras & Sensors afin que les autres utilisateurs puissent le trouver.

N’hésitez pas à solliciter la communauté pour obtenir de l’aide sur notre Discord 🤗

Update on GitHub