Управление роботом jetbot из ROS

В статье JETBOT на базе NVIDIA Jetson Nano мы рассмотрели робота jetson на на базе NVIDIA Jetson Nano. Сейчас рассмотрим установку пакетов ROS для управления и навигации на jetbot, чтобы превратить робота в удаленно управляемую движущую платформу.

Robot Operating System (ROS) — это гибкая платформа (фреймворк) для разработки программного обеспечения роботов. Это набор разнообразных инструментов, библиотек и определенных правил, целью которых является упрощение задач разработки ПО роботов.

Используем SD-карту 64 Гб, на которой записан образ NVIDIA JetPack. Образ NVIDIA JetPack основан на базе ОС Ubuntu 18.04. Будем устанавливать версию ROS Melodic (http://wiki.ros.org/melodic).

# Добавляем все репозитории Ubuntu:

 sudo apt-add-repository universe sudo apt-add-repository multiverse sudo apt-add-repository restricted

# добавляем репозиторий ROS 

 sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654

# установим ROS Base

 sudo apt-get update sudo apt-get install ros-melodic-ros-base 

# добавим пути ROS 

 sudo sh -c ' echo "source /opt/ros/melodic/setup.bash" >> ~ / .bashrc '

Установим библиотеки Adafruit для поддержки драйверов двигателя TB6612/PCA9685 и отладочного OLED-дисплея SSD1306:

# установка pip

 sudo apt-get install python-pip

# установка библиотек Adafruit

 pip install Adafruit-MotorHAT pip install Adafruit-SSD1306

Предоставим  пользователю доступ к шине i2c:

 sudo usermod -aG i2c $USER

Перезагрузим систему, чтобы изменения вступили в силу.

Создайте рабочее пространство ROS Catkin для хранения пакетов ROS:

 # создаем рабочую область catkin mkdir -p ~/workspace/catkin_ws/src cd ~/workspace/catkin_ws catkin_make # добавим путь catkin_ws к bashrc sudo sh -c 'echo "source ~/workspace/catkin_ws/devel/setup.bash" >> ~/.bashrc'
  

Закрываем и заново открываем новое окно терминала, чтобы убедиться, что  catkin_ws виден ROS:

 echo $ROS_PACKAGE_PATH 

Клонируем и собираем пакет jetson-inference. В пакете используется NVIDIA TensorRT для эффективного развертывания нейронных сетей на встроенной платформе Jetson.
 

 # установка git и cmake  sudo apt-get install git cmake # клонирование репозитория и подмодулей cd ~/workspace git clone https://github.com/dusty-nv/jetson-inference cd jetson-inference git submodule update --init # сборка из исходного кода  mkdir build cd build cmake ../ make 
 # установить библиотеки 
 sudo make install

Клонирование и сборка ROS пакет ros_deep_learning 

 # установить зависимости  sudo apt-get install ros-melodic-vision-msgs ros-melodic-image-transport ros-melodic-image-publisher # клонирование репозитория cd ~/workspace/catkin_ws/src git clone https://github.com/dusty-nv/ros_deep_learning # сборка catkin  cd ~/workspace/catkin_ws catkin_make
  

# проверка, что ROS находит пакет  что пакет ros_deep_learning

 rospack find ros_deep_learning

Клонирование и сборка ROS пакета  jetbot_ros

 # клонирование репозитория  cd ~/workspace/catkin_ws/src git clone https://github.com/dusty-nv/jetbot_ros  # сборка пакета   cd ~/workspace/catkin_ws $ catkin_make
 # проверка, что ROS находит пакет  что пакет  jetbot_ros  $ rospack найти jetbot_ros  

 Тестирование ros_jetbot Открываем терминал и запускаем  roscore Во втором терминале запускаем ноду jetbot_motors 

И тут проблема Import error: No module named Adafruit_MotorHAT, хотя библиотеку уже устанавливали (см. выше).
Но библиотека была установлена в python3, а ROS использует python2.7
Установим библиотеку Adafruit_MotorHAT для python2.7

 rosrun jetbot_ros jetbot_motors.py  И тут проблема Import error: No module named Adafruit_MotorHAT, хотя библиотеку уже устанавливали (см. выше). Но библиотека была установлена в python3, а ROS использует python2.7 Установим библиотеку Adafruit_MotorHAT для python2.7  python2.7 -m pip install Adafruit_MotorHAT Теперь запуск нормальный и в следующем терминале проверяем, что в ROS запущены соответствующие ноды и топики Вот список топиков

/jetbot_motors/cmd_dir — relative heading (degree [-180.0, 180.0], speed [-1.0, 1.0])

/jetbot_motors/cmd_raw — raw L/R motor commands (speed [-1.0, 1.0], speed [-1.0, 1.0])

/jetbot_motors/cmd_str — simple string commands (left/right/forward/backward/stop)

Но, к сожалению в jetbot_motors.py прописана обработка сообщений, получаемых из топика /jetbot_motors/cmd_str 

Пробуем отправлять их из терминала (это движения вперед, назад, влево, вправо и остановка)

 rostopic pub /jetbot_motors/cmd_str std_msgs/String --once "forward" rostopic pub /jetbot_motors/cmd_str std_msgs/String --once "backward" rostopic pub /jetbot_motors/cmd_str std_msgs/String --once "left" rostopic pub /jetbot_motors/cmd_str std_msgs/String --once "right" rostopic pub /jetbot_motors/cmd_str std_msgs/String --once "stop"

Опять проблема !!! Никаких признаков движения. Требуется внести изменения в файл jetbot_motors.py, изменить функции set_speed() и all_stop().

Я создал новый файл jetbot_motors_1.py и внес следующие изменения изменения

 # sets motor speed between [-1.0, 1.0] def set_speed(motor_ID, value):   max_pwm = 200.0   speed = int(min(max(abs(value * max_pwm), 0), max_pwm))   a = b = 0   if motor_ID == 1:     motor = motor_left     a=1     b=0   elif motor_ID == 2:     motor = motor_right     a=2     b=3   else:     rospy.logerror('set_speed(%d, %f) -> invalid motor_ID=%d', motor_ID, value, motor_ID)     return 	   motor.setSpeed(speed)    if value < 0:     motor.run(Adafruit_MotorHAT.FORWARD)     motor.MC._pwm.setPWM(a,0,0)     motor.MC._pwm.setPWM(b,0,speed*16)   elif value > 0:     motor.run(Adafruit_MotorHAT.BACKWARD)     motor.MC._pwm.setPWM(a,0,speed*16)     motor.MC._pwm.setPWM(b,0,0)   else:     motor.run(Adafruit_MotorHAT.RELEASE)     motor.MC._pwm.setPWM(a,0,0)     motor.MC._pwm.setPWM(b,0,0)   # stops all motors def all_stop(): 	set_speed(motor_left_ID, 0.0) 	set_speed(motor_right_ID, 0.0) 

Теперь jetbot реагирует на отправку сообщений, может двигаться вперед, назад, влево, вправо и стоп. Но это все с одной скоростью, регулируемой этим значением

 max_pwm = 200.0

Необходимо прописать реализацию движения с разной скоростью. Пока сделаем самое простое, будем использовать отправку сообщений std_msgs/String в топик /jetbot_motors/cmd_raw

 # raw L/R motor commands (speed, speed) def on_cmd_raw(msg): 	rospy.loginfo(rospy.get_caller_id() + ' cmd_raw=%s', msg.data)         speeds=msg.data.split(',') 	set_speed(motor_left_ID,  float(speeds[0])) 	set_speed(motor_right_ID, float(speeds[1]))  

И проверяем, отправляю подобные команды

 rostopic pub /jetbot_motors/cmd_raw std_msgs/String --once " 0.9,-0.7"

 

Теперь рассмотрим отправку информационных сообщений на OLED дисплей. 

Сначала установим библиотеку 

 python2.7 -m pip install Adafruit_SSD1306

Запускаем ноду jetbot_oled для отображения системной информации и пользовательского текста:

 rosrun jetbot_ros jetbot_oled.py

По умолчанию jetbot_oled экран обновляется каждую секунду с последними данными об использовании памяти, дискового пространства и IP-адресов.

Узел также будет прослушивать топик /jetbot_oled/user_text , чтобы получать строковые сообщения от пользователя и отображать на экране

 rostopic pub /jetbot_oled/ user_text std_msgs/String --once "HELLO!"

 

Использование камеры

Чтобы начать потоковую передачу с камеры JetBot, запускаем ноду  jetbot_camera

 rosrun jetbot_ros jetbot_camera

Видеокадры будут публиковаться в топик /jetbot_camera/raw в виде сообщений sensor_msgs::Image в кодировке BGR8. 

Установим пакет image_view , а затем подпишимся на него /jetbot_camera/raw с нового терминала:

 sudo apt-get install ros-melodic-image-view rosrun image_view image_view image:=/jetbot_camera/raw

После этого должно открыться окно, в котором будет отображаться видео с камеры в реальном времени. 

 

И еще отправку сообщений с тему /jetbot_motors/cmd_raw обязательно надо поменять. А именно тип сообщений на geometry_msgs/Twist, который широко используется в ROS.

Я создал новый файл jetbot_motors_2.py и внес следующие изменения изменения

 #!/usr/bin/env python import rospy import time import math   from Adafruit_MotorHAT import Adafruit_MotorHAT from std_msgs.msg import String from geometry_msgs.msg import Twist  PWM_MIN=0.5 PWM_MAX=1.0  # sets motor speed between [-1.0, 1.0] def set_speed(motor_ID, value):   max_pwm = 200.0   speed = int(min(max(abs(value * max_pwm), 0), max_pwm))   a = b = 0   if motor_ID == 1:     motor = motor_left     a=1     b=0   elif motor_ID == 2:     motor = motor_right     a=2     b=3   else:     rospy.logerror('set_speed(%d, %f) -> invalid motor_ID=%d', motor_ID, value, motor_ID)     return 	   motor.setSpeed(speed)    if value < 0:     motor.run(Adafruit_MotorHAT.FORWARD)     motor.MC._pwm.setPWM(a,0,0)     motor.MC._pwm.setPWM(b,0,speed*16)   elif value > 0:     motor.run(Adafruit_MotorHAT.BACKWARD)     motor.MC._pwm.setPWM(a,0,speed*16)     motor.MC._pwm.setPWM(b,0,0)   else:     motor.run(Adafruit_MotorHAT.RELEASE)     motor.MC._pwm.setPWM(a,0,0)     motor.MC._pwm.setPWM(b,0,0)   # stops all motors def all_stop(): 	set_speed(motor_left_ID, 0.0) 	set_speed(motor_right_ID, 0.0)  # directional commands (degree, speed) def on_cmd_dir(msg): 	rospy.loginfo(rospy.get_caller_id() + ' cmd_dir=%s', msg.data)  # raw L/R motor commands (speed, speed) def on_cmd_raw(msg): 	rospy.loginfo("msg cmd_raw") 	rospy.loginfo(msg)         x=max(min(msg.linear.x,1.0),-1.0)         z=max(min(msg.angular.z,1.0),-1.0)                  l=(x-z)/2         r=(x+z)/2 	#rospy.loginfo(x)         #rospy.loginfo(z) 	#rospy.loginfo(l)         #rospy.loginfo(r)                  lpwm= PWM_MIN+math.fabs(l)*(PWM_MAX-PWM_MIN)         rpwm= PWM_MIN+math.fabs(r)*(PWM_MAX-PWM_MIN)  	#rospy.loginfo(lpwm)         #rospy.loginfo(rpwm)         kl=1 if l>0 else -1         kr=1 if r>0 else -1         if l==0 : kl=0         if r==0 : kr=0  	set_speed(motor_left_ID, kl*lpwm ) 	set_speed(motor_right_ID, kr*rpwm)   # simple string commands (left/right/forward/backward/stop) def on_cmd_str(msg): 	rospy.loginfo(rospy.get_caller_id() + ' cmd_str=%s', msg.data)  	if msg.data.lower() == "left": 		set_speed(motor_left_ID,  -1.0) 		set_speed(motor_right_ID,  1.0)  	elif msg.data.lower() == "right": 		set_speed(motor_left_ID,   1.0) 		set_speed(motor_right_ID, -1.0)  	elif msg.data.lower() == "forward": 		set_speed(motor_left_ID,   1.0) 		set_speed(motor_right_ID,  1.0) 	elif msg.data.lower() == "backward": 		set_speed(motor_left_ID,  -1.0) 		set_speed(motor_right_ID, -1.0)   	elif msg.data.lower() == "stop": 		all_stop() 	else: 		rospy.logerror(rospy.get_caller_id() + ' invalid cmd_str=%s', msg.data)   # initialization if __name__ == '__main__':  	# setup motor controller 	motor_driver = Adafruit_MotorHAT(i2c_bus=1)  	motor_left_ID = 1 	motor_right_ID = 2  	motor_left = motor_driver.getMotor(motor_left_ID) 	motor_right = motor_driver.getMotor(motor_right_ID)  	# stop the motors as precaution 	all_stop()  	# setup ros node 	rospy.init_node('jetbot_motors') 	 	rospy.Subscriber('~cmd_dir', String, on_cmd_dir) 	rospy.Subscriber('~cmd_raw', Twist, on_cmd_raw) 	rospy.Subscriber('~cmd_str', String, on_cmd_str)  	# start running 	rospy.spin()  	# stop motors before exiting 	all_stop()

Ну и конечно, запуск всех нод запуском командного файла launch. Файл launch01.launch размещаем с catkin_ws/src/jetbot_ros/launch

 <launch>  <node name="jetbot_motors" pkg="jetbot_ros" type="jetbot_motors_2.py">  </node>  <node name="jetbot_camera" pkg="jetbot_ros" type="jetbot_camera">  </node>  <node name="jetbot_oled" pkg="jetbot_ros" type="jetbot_oled.py">  </node> </launch> 

Запуск командного файла

 roslaunch jetbot_ros launch01.launch

 

Сделаем управление роботом с удаленного компьютера. Будем использовать пакет rosbridge. Rosbridge позволяет внешним клиентам (в нашем случае браузеру) иметь доступ к темам и сервисам ROS (публикация и получение из тем, вызов сервисов). Rosbridge является частью мете-пакета rosbridge_suite, включающего различные дополнительные пакеты для реализации протокола rosbridge.

Пакет rosbridge_suite — это набор пакетов, реализующих протокол rosbridge и обеспечивающих транспортный уровень WebSocket.

В пакеты входят:

  • rosbridge_library — базовый пакет rosbridge. Rosbridge_library отвечает за получение строки JSON и отправку команд в ROS и наоборот.

  • rosapi — делает определенные действия ROS доступными через вызовы служб, которые обычно зарезервированы для клиентских библиотек ROS . Сюда входит получение и установка параметров, получение списка тем и многое другое.

  • rosbridge_server — обеспечивает соединение через WebSocket, чтобы браузеры могли «разговаривать с rosbridge». Roslibjs — это библиотека JavaScript для браузера, которая может взаимодействовать с ROS через rosbridge_server.

Установка пакета

 sudo apt-get install ros-melodic-rosbridge-suite

Создаем в папке проекта jetbot_ros в папке launch и командный файл launch03.launch

 <launch>  <include file="$(find rosbridge_server)/launch/rosbridge_websocket.launch"/>  <node name="jetbot_motors" pkg="jetbot_ros" type="jetbot_motors_2.py">  </node>  <node name="jetbot_camera" pkg="jetbot_ros" type="jetbot_camera">  </node>  <node name="jetbot_oled" pkg="jetbot_ros" type="jetbot_oled.py">  </node>  </launch>  

И запуск командного файла

1 терминал

 roscore

2 терминал

 roslaunch jetbot_ros launch03.launch

Для организации web-интерфейса необходимо установить web-сервер.

 sudo apt-get install apache2

Теперь в папке /var/www/html будет находиться наша страница html. Библиотеку roslib.min.js поместим в папке js.

Создадим html файл index01.html, где подключимся к ROS по websocket 9090 и будем отправлять из формы сообщения в тему ROS /pub_txt_msg и получать и отображать на странице сообщения, приходящие в тему ROS /sub_txt_msg

 <!DOCTYPE html> <html> <head> <meta charset="utf-8" />  <script type="text/javascript" src="js/roslib.min.js"></script> <script type="text/javascript" type="text/javascript">   var ros = new ROSLIB.Ros({     url : 'ws://192.168.0.111:9090'   });    ros.on('connection', function() {     document.getElementById("status").innerHTML = "Connected";   });    ros.on('error', function(error) {     document.getElementById("status").innerHTML = "Error";   });    ros.on('close', function() {     document.getElementById("status").innerHTML = "Closed";   });   var txt_listener = new ROSLIB.Topic({     ros : ros,     name : '/sub_txt_msg',     messageType : 'std_msgs/String'   });    txt_listener.subscribe(function(m) {     document.getElementById("msg").innerHTML = m.data;   });    var pub1=new ROSLIB.Topic ({      ros: ros,      name : '/pub_txt_msg',      messageType : 'std_msgs/String'     });   function send_ros() {     var msg=new ROSLIB.Message ({"data" : document.getElementById("putdata").value});     pub1.publish(msg);   }  </script> </head>  <body>   <h1>Simple ROS User Interface</h1>   <p>Connection status: <span id="status"></span></p>   <p>Last /txt_msg received: <span id="msg"></span></p>   <p><form id=formoptions name=formoptions action="javascript:void();" onsubmit="feturn false;">       send to /pub_txt_msg <br>       <input  name=putdata id=putdata>       <button id='button1' value='send' onclick='send_ros();'>Send</button>     </form>   </p>   </body> </html> 

Запускаем в браузере страницу index01.html

Сообщения в тему /sub_txt_msg

 rostopic pub /sub_txt_msg std_msgs/String "Hello? browser"

Просмотр сообщений, отправляемых из браузера в тему /pub_txt_msg

 rostopic echo /pub_txt_msg

В дальнейшем запускать все ноды будем из командных файлов

Вывод потокового изображения с камеры на web-страницу

Для вывода потокового изображения с камеры на web-страницу установим ros-пакет web_video_server

 sudo apt-get install ros-melodic-web-video-server

Создаем командный файл launch03.launch

 <launch>   <include file="$(find rosbridge_server)/launch/rosbridge_websocket.launch"/>   <node name="jetbot_motors" pkg="jetbot_ros" type="jetbot_motors_2.py">   </node>  <node name="jetbot_oled" pkg="jetbot_ros" type="jetbot_oled.py">   </node>   <node name="jetbot_camera" pkg="jetbot_ros" type="jetbot_camera">   </node>   <node name="web_video_server" pkg="web_video_server" type="web_video_server">     <param name="port" value="8090"></param>     <param name="address" value="192.168.0.111"></param>   </node> </launch>

Просмотр видео на странице

Создаем файл index02.html для управления движением робота и просмотра изображения с камеры. Используем библиотеку nipplejs.js

 <!DOCTYPE html> <html> <head> <meta charset="utf-8" />  <script type="text/javascript" src="js/roslib.min.js"></script> <script type="text/javascript" src="js/nipplejs.js"></script> <script type="text/javascript" type="text/javascript">   var ros = new ROSLIB.Ros({     url : 'ws://192.168.0.111:9090'   });    ros.on('connection', function() {     document.getElementById("status").innerHTML = "Connected";   });    ros.on('error', function(error) {     document.getElementById("status").innerHTML = "Error";   });    ros.on('close', function() {     document.getElementById("status").innerHTML = "Closed";   });   var txt_listener = new ROSLIB.Topic({     ros : ros,     name : '/txt_msg',     messageType : 'std_msgs/String'   });    txt_listener.subscribe(function(m) {     document.getElementById("msg").innerHTML = m.data;   });    cmd_vel_listener = new ROSLIB.Topic({     ros : ros,     name : "/cmd_vel",     messageType : 'geometry_msgs/Twist'   });    move = function (linear, angular) {     var twist = new ROSLIB.Message({       linear: {         x: linear,         y: 0,         z: 0       },       angular: {         x: 0,         y: 0,         z: angular       }     });     cmd_vel_listener.publish(twist);   }       createJoystick = function () {       var options = {         zone: document.getElementById('zone_joystick'),         threshold: 0.1,         position: { left: 50 + '%' },         mode: 'static',         size: 150,         color: '#000000',       };       manager = nipplejs.create(options);       linear_speed = 0;       angular_speed = 0;       manager.on('start', function (event, nipple) {         timer = setInterval(function () {           move(linear_speed, angular_speed);         }, 25);       });       manager.on('move', function (event, nipple) {         max_linear = 1.0; // m/s         max_angular = 1.0; // rad/s         max_distance = 75.0; // pixels;         linear_speed = Math.sin(nipple.angle.radian) * max_linear * nipple.distance/max_distance; 				angular_speed = -Math.cos(nipple.angle.radian) * max_angular * nipple.distance/max_distance;       });       manager.on('end', function () {         if (timer) {           clearInterval(timer);         }         self.move(0, 0);       });     }     window.onload = function () {       createJoystick();     }  </script> </head>  <body>   <h1>Simple ROS User Interface</h1>   <p>Connection status: <span id="status"></span></p>   <p>Last /txt_msg received: <span id="msg"></span></p>   <div id="zone_joystick" style="position: relative;"></div> </body> </html> 

После запуска страницы можем управлять движением робота. Вид страницы

Если необходимо добавить управление с джойстика, то создадим скрипт subscriber, ловящий данные, публикуемые пакетом joy и преобразующий их в данные для ноды /jetbot_motors/cmd_raw.

Содержимое скрипта jetbot_joy.py

 #!/usr/bin/env python import rospy import time import math   from sensor_msgs.msg import Joy from std_msgs.msg import String from geometry_msgs.msg import Twist  pub=rospy.Publisher("/jetbot_motors/cmd_raw",Twist)  def controller(data):     rospy.loginfo(str(data.axes[0])+" "+str(data.axes[1]))     msg=Twist()     msg.linear.x=data.axes[0];     msg.linear.y=0.0;     msg.linear.z=0.0;     msg.angular.x=0.0;     msg.angular.y=0.0;     msg.angular.z=data.axes[1];     pub.publish(msg)       def listener():      rospy.init_node('jetbot_joy')      rospy.sleep(1.0)           sub = rospy.Subscriber("joy",Joy,controller)      rospy.spin()    if __name__ == '__main__':    listener()

 

И командный файл launch04.launch

 <launch>  <include file="$(find rosbridge_server)/launch/rosbridge_websocket.launch"/>  <node name="jetbot_motors" pkg="jetbot_ros" type="jetbot_motors_2.py">  </node>  <node name="jetbot_oled" pkg="jetbot_ros" type="jetbot_oled.py">  </node>  <node name="jetbot_camera" pkg="jetbot_ros" type="jetbot_camera">  </node>  <node name="joy_node" pkg="joy" type="joy_node">  </node>  <node name="jetbot_joy" pkg="jetbot_ros" type="jetbot_joy.py">  </node>  <node name="web_video_server" pkg="web_video_server" type="web_video_server">   <param name="port" value="8090"></param>   <param name="address" value="192.168.0.36"></param>  </node>  </launch>