Клиент Rust для ROS2

Создайте клиент Rust для ROS2 с нуля. Часть 0: интеграция C API для создания узла ROS2

Свяжите ROS2 C API с Rust через bindgen и создайте узел в Rust. Простые шаги по использованию `bindgen`.

Это первая статья из этой серии: Клиент Rust для ROS2 с нуля.

Остальные части: 1.1 Создание динамической библиотеки с помощью CMake & Empy

Фон

ROS2 - это последняя версия ROS (Robot Operating System), которая составляет основу для создания приложений, связанных с робототехникой, а также предоставляет множество библиотек и инструментов для таких приложений. Основным преимуществом перехода от ROS к ROS2 является то, что ROS2 использует DDS для своей системы связи. Для использования DDS не требуется master node в ROS, что упрощает протокол связи. Для получения дополнительной информации о преимуществах и различиях Использование ROS2 и Почему ROS2? Статья проливает свет на особенности ROS2.

Поскольку мы используем ROS2 в роботах и ​​приложениях для роботов, очевидно, что нам нужно написать программное обеспечение другого уровня для управления роботом. Например, нам нужно использовать встроенный код для управления оборудованием и, кроме того, нам нужно использовать код более высокого уровня для выполнения таких решений, как планирование пути, а также управление поведением. В настоящее время большая часть встроенного кода использует C / C ++, а код более высокого уровня использует Python или C / C ++.

Несмотря на то, что Rust был разработан в течение этого десятилетия, он уже используется в различных областях программирования, таких как создание сетей, веб-разработка, веб-сборка, встроенная система и т. Д. Rust позволяет нам использовать более быстрый и безопасный код. По сравнению с C / C ++, Rust работает почти с той же скоростью, но его преимущество в том, что он решает наиболее болезненную часть C / C ++, а именно segmentation fault. По сравнению с Python, Rust намного быстрее (пожалуйста, ознакомьтесь с моей предыдущей статьей Сравнение производительности: Rust против PyO3 против Python). Еще одно преимущество Rust перед Python заключается в том, что он обеспечивает лучшую интеграцию и плавное управление кодовой базой по мере ее роста. Управлять растущей базой кода с помощью Python сложно.

Это приводит к вопросу: почему мы не используем Rust в мире ROS2? На самом деле существует несколько библиотек, написанных на Rust. Существует также одна библиотека под названием ros2_rust, которая пыталась создать клиент Rust для ROS2, но, к сожалению, она не активна - после 2017 года было зарегистрировано всего 7 коммитов. В настоящее время я работаю над различными проектами робототехники, которые используют Python в качестве основного языка. в рамках моей работы инженером-программистом робототехники. Учитывая объем кода, я действительно чувствую боль от поддержки кодовой базы Python. Кроме того, по мере роста кодовой базы становится трудно поддерживать. Прочитав статьи и изучив Rust, а также написав код на Rust, я считаю, что Rust идеально подходит для приложений и программ робототехники. Он достаточно быстр для управления высокоскоростным оборудованием (используемым в робототехнике), а также имеет меньше ошибок после того, как компилятор проанализирует код.

Это подводит нас к вопросу этой статьи: почему бы нам не начать создавать клиент Rust для ROS2? В этой серии статей мы создадим клиент Rust с нуля. Для завершения этой работы может потребоваться некоторое время, так как я работаю над ней как неполный рабочий день, а также учусь в процессе. Преимущества работы с Rust будут огромными и помогут любому, кто хотел бы интегрировать язык в свои приложения или код для робототехники.

Цель части 0 (код привязки и создание узла)

Одно из основных преимуществ ROS2 (как отмечалось ранее) заключается в том, что ее не нужно повторно внедрять с нуля. ROS2 поставляется с хорошим встроенным интерфейсом Клиентская библиотека ROS (RCL), который реализует логику и поведение концепций ROS. Нам просто нужно написать оболочку Rust, которая похожа на rclpy.

В каждой части этой серии я в основном сосредоточусь на одном ключевом моменте ROS2. В части 0 мы сделаем следующее:

  • Свяжите код ROS2 C с Rust.
  • Используйте связанный код для создания узла ROS2.

Установки / Предварительные условия

Связывание кода C ROS2 с Rust

Rust использует FFI (интерфейс внешних функций) для взаимодействия с C / C ++, но если библиотека велика, написание кода FFI становится монотонным, поскольку оно включает простое копирование и вставку каждой функции из одного места в другое. Мы используем знаменитый и полезный ящик bindgen в Rust, чтобы помочь нам создать код FFI. Чтобы узнать больше о bindgen, прочтите bindgen руководство пользователя. Шаги по связыванию кода с Rust следующие:

1. Создайте грузовой проект и добавьте build-dependencies в Cargo.toml.

[build-dependencies]
bindgen = “0.54.1”

2. Создайте wrapper.h файл, содержащий все библиотеки, которые мы хотим связать в корне проекта. Нам нужно только привязать rcl.h здесь.

#include <rcl/rcl.h>

3. Создайте файл build.rs в корне проекта, чтобы компилятор выполнил его перед построением остальной части контейнера. Важным параметром, который необходимо добавить в build.rs, является указание rustc связываться с ROS2. Полный код можно найти здесь.

// Include header files which `rcl.h` used.
builder = builder.clang_arg(format!(“-I/opt/ros/eloquent/include”));
println!(“cargo:rustc-link-search=/opt/ros/eloquent/lib”);
// Tell cargo to tell rustc to link the system bzip2
// shared library.
println!(“cargo:rustc-link-lib=rcl”);
println!(“cargo:rustc-link-lib=rcutils”);
// Tell cargo to invalidate the built crate whenever the wrapper changes
println!(“cargo:rerun-if-changed=wrapper.h”);

4. Создайте наш mod bindings, который включает сгенерированный код привязки. Поскольку мы определили место сохранения нового файла ржавчины в $OUR_DIR/bindings.rs, мы включаем его в наш mod.

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
// Allow warning:
// `128-bit integers don’t currently have a known stable ABI`
#![allow(improper_ctypes)]
include!(concat!(env!(“OUT_DIR”), “/bindings.rs”));

5. Теперь мы можем попробовать собрать код.

$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.02s

НО не забудьте включить source /opt/ros/eloquent/setup.bash, который загрузит ROS2 env.

Можно также попробовать включить cargo test, после чего вы увидите, что весь код библиотеки и интеграционных тестов проходит. (Примечание. Провал некоторых тестов - это нормально)

6. Вызовите функцию ROS2 в нашем main.rs, чтобы убедиться, что мы успешно связали ROS2 с Rust.

use bindings::*;
mod bindings;
fn main() {
    let mut node = unsafe { rcl_get_zero_initialized_node() };
    println!(“node: {:?}”, node);
}

cargo run результат:

$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 1.22s
 Running `target/debug/test`
node: rcl_node_t { context: 0x0, impl_: 0x0 }

После выполнения нашего кода мы можем успешно создать node, что означает, что теперь мы можем использовать большую часть кода ROS2 RCL с FFI. Я говорю «большинство», потому что некоторые функции связаны с другими библиотеками в ROS2, которые мы не связали в файле build.rs.

Кроме того, если вас интересует, какой код FFI генерирует bindgen, вы можете просмотреть $OUR_DIR/bindings.rs самостоятельно. Поскольку он слишком длинный, я не буду его здесь показывать.

Используйте код привязки для создания узла ROS2

node - одно из основных понятий в ROS. Проще говоря, node - это процесс, и у него есть несколько издателей и несколько подписчиков, которые позволяют ему общаться с другими node. node похож на class в ООП, который имеет несколько API, но node использует другие сообщения вместо этих вызовов API.

Чтобы прочитать документ в node.h файле ROS2, нам нужно использовать несколько функций для создания живого node. («Live» здесь означает, что node может быть обнаружен ROS2 или другими узлами) Ожидаемое использование для вышеуказанного в C показано ниже:

rcl_context_t context = rcl_get_zero_initialized_context();
// … initialize the context with rcl_init()
rcl_node_t node = rcl_get_zero_initialized_node();
rcl_node_options_t node_ops = rcl_node_get_default_options();
// … node options customization
rcl_ret_t ret = rcl_node_init(&node, “node_name”, “/node_ns”, &context, &node_ops);
// … error handling and then use the node, but eventually deinitialize it:
ret = rcl_node_fini(&node);
// … error handling for rcl_node_fini()

Интуитивно нам нужно «скопировать» приведенный выше код в Rust. Код C в Rust «небезопасен», поэтому нам нужно использовать блок unsafe при их использовании. Мы можем добавить показанный ниже код в main.rs и проверить вывод функции rcl_node_init. Если он компилируется правильно, rcl_node_init вернет 0, что означает хороший результат.

let mut context = unsafe { rcl_get_zero_initialized_context() };
let mut node = unsafe { rcl_get_zero_initialized_node() };
let node_ops = unsafe { rcl_node_get_default_options() };
let node_name = CString::new(“nodename”).unwrap();
let node_ns = CString::new(“nodens”).unwrap();
unsafe {
    let ret = rcl_node_init(
        &mut node as *mut _,
        node_name.as_ptr(),
        node_ns.as_ptr(),
        &mut context as *mut _,
        &node_ops as *const _,
    );
    println!(“*** node init: {:?} ***”, ret); // should return `0`
    rcl_node_fini(&mut node as *mut _);
}

После запуска кода мы видим следующий результат.

*** node init: 101 ***

Очевидно, что даже несмотря на то, что наш код работал и вызывал rcl_node_init функцию, он каким-то образом возвращает код 101 вместо 0. Таким образом, нам нужно исправить это 101. После прохождения кода 101 код означает rcl_init() not yet called. Итак, нам нужно позвонить rcl_init перед вызовом rcl_node_init.

Выполнив те же шаги для проверки документа rcl_init, мы можем использовать следующий полный код Rust для создания узла.

Я использую команду ros2 node list перед rcl_node_fini, чтобы убедиться, что узел работает должным образом. Мы также можем выполнить ту же команду в терминале, после чего мы увидим, что наш node запущен и ROS2 может его увидеть.

Теперь наш узел работает, как ожидалось, и, кроме того, ROS2 может его распознать. Наша первая цель достигается подключением и вызовом C API с помощью Rust. Поскольку первая цель в основном связана с кодом, я не добавил сюда никаких тестов. Мы можем добавить тесты после того, как добавим больше функций.

Заключение

Rust многообещающий в мире робототехники. Это определенно будет играть огромную роль в будущем, особенно когда дело доходит до интеграции с существующими языками, такими как ROS2, учитывая высокий рост и принятие ROS2 сейчас. Кроме того, есть много новых реализаций для оптимизации существующих алгоритмов ROS. Если мы сможем объединить и то, и другое вместе, Rust станет одним из наиболее распространенных и полезных языков в робототехнике и робототехнических приложениях. Более того, Rust также может взять на себя роль C / C ++ и стать фундаментальным языком для разработки робототехники с полным стеком.

Эта статья - моя первая статья из этой серии. Я постараюсь публиковать эту серию каждые 2 недели или каждый месяц. Надеюсь, вам понравится читать его, учиться на нем и предлагать подходящие отзывы, основанные на вашем использовании. Исходный код находится в репозитории: https://github.com/marshalshi/rus2, и конечная цель - создать полностью поддерживаемый клиент Rust для ROS2.

Спасибо моему другу VarunD за просмотр статьи и ее улучшение.