65.9K
CodeProject 正在变化。 阅读更多。
Home

Rodney - 漫长的自主机器人之路(第六部分)。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2019 年 4 月 15 日

Apache

22分钟阅读

viewsIcon

11503

downloadIcon

512

关于 ROS(机器人操作系统)家用机器人的系列文章的第六部分。

引言

Rodney 机器人项目是一个业余机器人项目,旨在设计和构建一个使用 ROS(机器人操作系统)的自主家用机器人。本文是描述该项目的系列文章中的第六篇。

背景

第 1 部分中,为了帮助定义我们机器人的需求,我们选择了我们的第一个任务,并将其分解为多个设计目标,使其更易于管理。

该任务取自文章让我们建造一个机器人!,任务是:给……传达消息 - 既然机器人将[拥有]识别家庭成员的能力,那么它能否充当“消息传递者和提醒者”呢?我可以对机器人说“机器人,提醒(人名)在下午 6 点到车站接我”。然后,即使那个家庭成员的手机处于静音状态,或者正在听嘈杂的音乐,或者(插入任何不来车站接我的理由),机器人也可以在房子里漫游,找到那个人,并向他们传达消息。

该任务的设计目标是

  • 设计目标1:能够使用摄像头环顾四周,搜索人脸,尝试识别任何看到的人,并显示识别出来的信息
  • 设计目标2:面部表情和语音合成。罗德尼需要能够传递信息
  • 设计目标3:通过远程键盘和/或操纵杆控制移动
  • 设计目标4:增加激光测距仪或类似的测距传感器以辅助导航
  • 设计目标5:自主移动
  • 设计目标6:任务分配和完成通知

在上一部分中,我们添加了电机控制和里程计反馈来完成设计目标 3。在这一部分中,我将添加一个旋转 LIDAR(光检测和测距)来完成设计目标 4,并添加一个 IMU 以改进里程计。在添加 IMU 的过程中,我还会将 Arduino Nano 更换为 Teensy 3.5。

添加 LIDAR

ROS 中自主导航最常用的方法需要订阅 /scan/tf 主题。/tf 主题用于获取里程计变换,我们已在上一篇文章中开始从 ekf_localization_node 广播它。/scan 主题包含来自激光扫描设备的数据。

您可以在开发中的自动驾驶汽车上看到一些大型且非常昂贵的 360 度扫描设备,但由于 Rodney 将被限制在房子里,我们可以大幅降低价格范围。Slamtec 生产了许多旋转 LIDAR,他们的RPLidar A1 价格合理,约为 100 英镑。RPLidar 的量程为 12 米,提供完整的 360 度扫描,并使用串行接口进行通信。但还有更好的,他们甚至开发了一个 ROS 节点,可以从 Slamtec GitHub 页面下载。因此,我们可以将此设备直接插入我们的支持 ROS 的机器人,并开始在 /scan 主题上接收消息,而无需弄清楚它正在传输什么串行数据。

下面是安装在 Rodney 上的 RPLidar 的照片。

我购买的 RPLidar 版本是开发套件,带有一个 USB 串口设备和连接此设备到 LIDAR 的电缆。除了 Tx/Rx 数据线,它还为 LIDAR 电机和 LIDAR 核心提供 5V 电源。还有一个 PWM 输入到核心,可用于控制电机速度,如果您使用提供的 USB 串口设备,此线连接到 DTR 信号,可用于启用/禁用设备。

由于我试图在 Rodney 上不同的电源之间分配负载,我决定从不同的电源为电机和核心供电,并且只通过 USB 电源从 Raspberry Pi 为 USB 串口设备供电。因此,我制作了一个简单的分线板,以便我可以单独为 LIDAR 供电。下图显示了安装 LIDAR 的平台后部的分线板和 USB 串口设备。单独的电源通过照片中未连接的 3 个引脚接入。平台和支架的 stl 文件可在本文随附的 3D 打印 zip 文件中找到。

现在,一直关注这些文章的一些人可能已经发现了一个问题。尽管我将 RPLidar 安装得足够高,以便后端平台上的电子设备低于其激光器,但 Rodney 的脖子会挡住激光器,从而导致在 360 度视野中始终检测到。有一个现有的名为 laser filters 的 ROS 包,它允许我们使用一个名为 LaserScanAngularBoundsFilter 的插件来移除消息中覆盖颈部区域的点。我们将使用它来移除 180 度标记两侧各 10 度的点。

为了添加 RPLidar 的代码,我们将编辑本文前面部分创建的一些包,即 rodneyrodney_missions

激光滤波器节点将从我们的 rondey.launch 文件启动,并且需要一个我们将加载到 ROS 参数服务器的配置文件。因此,我们需要将以下名为 laser_filter_config.yaml 的文件添加到 rodney/config 文件夹中。

scan_filter_chain:
- name: angle
  type: laser_filters/LaserScanAngularBoundsFilter
  params:
    lower_angle: -2.96706
    upper_angle: 2.96706

下限和上限角度以弧度表示,这将把我们的视野限制在 0-170 度和 0+170 度之间。

为了将这些参数加载到参数服务器并启动扫描过滤器,我们将在 rodney/launch 文件夹中的 rodney.launch 文件中添加以下几行。我们将对启动文件进行更多更改,因此我将在本文后面完整展示它。

<node pkg ="laser_filters" type="scan_to_scan_filter_chain" 
 name="scan_to_scan_filter_chain" output="screen">
  <rosparam command="load" file="$(find rodney)/config/laser_filter_config.yaml"/>
  <remap from="scan" to="scan_filter_input"/>
  <remap from="scan_filtered" to="scan"/>
</node>

从上面可以看出,我们需要对主题名称进行一些重新映射,节点的输出主题是 scan_filtered,但导航包将订阅 scan。我们还将 RPLidar 发布的主题从 scan 重新映射到 scan_filter_input

接下来,我们需要启动 Slamtec 提供的 RPLidar 节点。这里有一个小问题,提供的串行设备被 Linux 系统识别为 /dev/ttyUSBn。这是一个常见的名称,如果您的系统中有一个以上名为 ttyUSBn 的设备,可能会使系统混淆。

我们可以通过添加一些 udev 规则来避免这种不确定性,这些规则将创建一组符号链接,我们将在启动文件中使用这些链接。在 rodney/scripts 文件夹中,我在 rodney_udev.rules 文件中创建了 udev 规则。

# Set the udev rules. 
#
# Arduino Nano
KERNEL=="ttyUSB*", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", MODE:="0777", SYMLINK+="nano"
#
# Teensy
KERNEL=="ttyACM*", ATTRS{idVendor}=="16c0", ATTRS{idProduct}=="0483", MODE:="0777", SYMLINK+="teensy"
#
# RPLidar
KERNEL=="ttyUSB*", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", MODE:="0777", SYMLINK+="rplidar"

使用 sudo 复制命令,将此文件复制到 Raspberry Pi 上的 /etc/udev/rules.d 文件夹。我还创建了一个名为 create_udev_rules.sh 的脚本文件,它将为您完成复制。

接下来,我们可以将启动 RPLidar 节点的命令添加到 rodey.launch 文件中。

<node pkg="rplidar_ros" type="rplidarNode" name="rplidar_node" output="screen">
  <param name="serial_port" type="string" value="/dev/rplidar"/>
  <param name="serial_baudrate" type="int" value="115200"/>
  <param name="frame_id" type="string" value="laser"/>
  <remap from="scan" to="scan_filter_input"/>
</node>

我们将在本文后面的“使用代码”部分构建和运行所有代码,但下图显示了在 rviz 中可视化的激光扫描消息。

IMU(惯性测量单元)

在第 5 部分中,我们开始广播源自电机编码器的原始里程计数据,并包含了 ekf_localization_node,我们说它将用于将原始里程计与 IMU 数据融合以改进机器人的里程计。

对于 IMU,我们将使用 SparkFun MPU-9250 分线板。

我想将 IMU 连接到微控制器,而不是直接连接到 Raspberry Pi。由于 Arduino Nano 内存已经不足,我们需要一个替代解决方案。您可以向项目中添加第二个 Nano 或使用更大的 Arduino 板,但我决定在我的项目中将 Nano 替换为 Teensy 3.5。Teensy 比 Arduino 更快,内存也更多,但与 Arduino 软件和库兼容。您可以下载一个插件,以便您可以继续使用 Arduino IDE 进行开发。我选择了 Teensy 3.5,因为它与更快的 3.6 不同,其数字输入支持 5V 电压。

下图显示了图像下部的 IMU(MPU-9250 分线板),Teensy 显示在左上角。

Arduino 草图更新

由于 Teensy 速度更快,我们也将趁此机会提高与 Raspberry Pi 串行接口的波特率。随着机器人的发展,我们可能还需要在 Teensy 和 Pi 之间传输更大的消息。为了更改缓冲区大小和波特率,您需要更改 ROS 串行库中的 ros.hArduinoHardware.h 文件。您可以直接在库中进行更改,但如果您重新编译库,例如添加新消息,那么这些更改将丢失。因此,我已在 sketch 文件夹中重新创建了这些文件。

该草图仍然包含伺服电机和霍尔传感器的代码,但已更新以包含 IMU 功能。

setup 函数中,我们现在还确保可以与 IMU 通信,并从我使用 Library Manager 安装到 Arduino IDE 的 SparkFun MPU-9250 9 DOF IMU Breakout 库中调用设置过程。请注意,这里我们还设置了 IMU 的磁力计部分,尽管我们目前没有在任何主题上广播磁力计数据。在 setup 函数的末尾,我们现在只有在成功设置 IMU 后才打开板载 LED。

loop 函数中,我添加了代码,如果 IMU 设置失败,则记录一条消息。以前,我发现草图 setup 部分中进行的任何日志调用都不会被记录。在每次调用 loop 期间,我们都会检查 IMU 寄存器是否包含新数据,如果是,则读取加速度计、陀螺仪和磁力计数据。如果到了发布 IMU 数据的时间,我们就会形成一个 sensor_msgs/Imu 消息并在 imu/data_raw 主题上广播它。

rodney_control.ino

#include <PWMServo.h> // Use PWMServo on Teensy
#include <MPU9250.h>

// Use "ros.h" not <ros.h> so that by using our local version 
// we can increase/decrease buffer size if required and 
// increased the baud rate on faster boards.
#include "ros.h"
#include <servo_msgs/servo_array.h>
#include <tacho_msgs/tacho.h>
#include <sensor_msgs/Imu.h>
#include <sensor_msgs/MagneticField.h>

void servo_cb( const servo_msgs::servo_array& cmd_msg);
void WheelSpeed0();
void WheelSpeed1();

#define LED_PIN 13  // Onboard LED

#define GEAR_BOX_COUNTS_PER_REV 1440.0f

// Define the period in milliseconds between tacho messages
#define TACHO_PERIOD_MS 50  // Publish at 20Hz

// Define the PWM pins that the other servos are connected to
#define SERVO_0 23
#define SERVO_1 22
#define SERVO_2 21
#define SERVO_3 20

// Define pins used for two Hall sensors
#define ENCODER0_PINA 0  // Interrupt
#define ENCODER0_PINB 1  // Digital pin
#define ENCODER1_PINA 3  // Interrupt
#define ENCODER1_PINB 4  // Digital pin

PWMServo  servo0;
PWMServo  servo1;
PWMServo  servo2;
PWMServo  servo3;

#define G_TO_MS2 9.80665

#define I2Cclock 400000
#define I2Cport Wire
#define MPU9250_ADDRESS MPU9250_ADDRESS_AD0
MPU9250 myIMU(MPU9250_ADDRESS, I2Cport, I2Cclock);

tacho_msgs::tacho tachoMsg;
sensor_msgs::Imu  imuMsg;  

ros::NodeHandle nh;

ros::Publisher tachoPub("tacho", &tachoMsg);
ros::Publisher imuPub("imu/data_raw", &imuMsg);
ros::Subscriber<servo_msgs::servo_array> subServo("servo", servo_cb);

bool  imuTestPassed;
byte  encoder0PinALast;
byte  encoder1PinALast;
volatile int encoder0Count; // Number of pulses
volatile int encoder1Count; // Number of pulses
volatile boolean encoder0Direction; //Rotation direction
volatile boolean encoder1Direction; //Rotation direction
unsigned long publisherTime;
unsigned long currentTime;
unsigned long lastTime;
char imu_link[] = "imu";

void setup() 
{
  Wire.begin();
  
  nh.initNode();
  nh.advertise(tachoPub);
  nh.advertise(imuPub);
  nh.subscribe(subServo);

  // Attach servos
  servo0.attach(SERVO_0); //attach it to the pin
  servo1.attach(SERVO_1);
  servo2.attach(SERVO_2);
  servo3.attach(SERVO_3);
  servo0.write(90);
  servo1.write(120);
  servo2.write(90);
  servo3.write(90);

  encoder0Direction = true;   // default is forward
  encoder1Direction = true;
  encoder0Count = 0;
  encoder1Count = 0;

  pinMode(ENCODER0_PINB, INPUT);
  pinMode(ENCODER1_PINB, INPUT);

  // Attach the interrupts for the Hall sensors
  attachInterrupt(digitalPinToInterrupt(ENCODER0_PINA), WheelSpeed0, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENCODER1_PINA), WheelSpeed1, CHANGE);

  imuTestPassed = true;
  
  // Read the WHO_AM_I register of the IMU, this is a good test of communication
  byte c = myIMU.readByte(MPU9250_ADDRESS, WHO_AM_I_MPU9250);
  
  if(c == 0x71) // WHO_AM_I should always be 0x71
  { 
    // Start by performing self test
    myIMU.MPU9250SelfTest(myIMU.selfTest);
    
    for(int i = 0; i < 6; i++)
    {
      if(abs(myIMU.selfTest[i]) > 14.0f)
      {
        imuTestPassed = false;
      }
    }

    // Calibrate gyro and accelerometers, load biases in bias registers
    myIMU.calibrateMPU9250(myIMU.gyroBias, myIMU.accelBias);

    // Initialize device for active mode read of acclerometer, gyroscope, and temperature
    myIMU.initMPU9250();

    // Read the WHO_AM_I register of the magnetometer, this is a good test of communication
    byte d = myIMU.readByte(AK8963_ADDRESS, WHO_AM_I_AK8963);

    if(d == 0x48)
    {
      // Get magnetometer calibration from AK8963 ROM
      // Initialize device for active mode read of magnetometer
      myIMU.initAK8963(myIMU.factoryMagCalibration);

      // Get sensor resolutions, only need to do this once
      myIMU.getAres();
      myIMU.getGres();
      myIMU.getMres();
    }
    else
    {
      imuTestPassed = false;
    }
  }
  else
  {
    imuTestPassed = false;
  }

  if(imuTestPassed == true)
  {
    // Turn on the onboard LED to show we are running 
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, HIGH);
  }
}

void loop()
{
  static bool setup = false;

  if(setup == false)
  {
    // Log only gets reported in loop
    nh.loginfo("Teensy code started");

    if(imuTestPassed == false)
    {
      nh.loginfo("IMU self test failed");        
    }
    
    setup = true;
  }
  
  // Is it time to publish the tacho message
  if(millis() > publisherTime)
  {
    float deltaTime;
    
    currentTime = micros();
    deltaTime = (float)(currentTime - lastTime)/1000000.0;

    // Right wheel speed
    tachoMsg.rwheelrpm = 
         (((((float)encoder0Count)/2.0f)/deltaTime)/GEAR_BOX_COUNTS_PER_REV)*60.0f;
    encoder0Count = 0;

    // Left wheel speed
    tachoMsg.lwheelrpm = 
         (((((float)encoder1Count)/2.0f)/deltaTime)/GEAR_BOX_COUNTS_PER_REV)*60.0f;
    encoder1Count = 0;

    lastTime = currentTime;
    
    tachoPub.publish(&tachoMsg);
    publisherTime = millis() + TACHO_PERIOD_MS;
  }

  // IMU 
  if(imuTestPassed == true)
  {
    // Check to see if all data registers have new data
    if (myIMU.readByte(MPU9250_ADDRESS, INT_STATUS) & 0x01)
    {
      myIMU.readAccelData(myIMU.accelCount);  // Read the x/y/z adc values

      // Now we'll calculate the accleration value into actual g's
      // This depends on scale being set
      myIMU.ax = (float)myIMU.accelCount[0] * myIMU.aRes;
      myIMU.ay = (float)myIMU.accelCount[1] * myIMU.aRes;
      myIMU.az = (float)myIMU.accelCount[2] * myIMU.aRes;

      myIMU.readGyroData(myIMU.gyroCount);  // Read the x/y/z adc values

      // Calculate the gyro value into actual degrees per second
      // This depends on scale being set
      myIMU.gx = (float)myIMU.gyroCount[0] * myIMU.gRes;
      myIMU.gy = (float)myIMU.gyroCount[1] * myIMU.gRes;
      myIMU.gz = (float)myIMU.gyroCount[2] * myIMU.gRes;

      myIMU.readMagData(myIMU.magCount);  // Read the x/y/z adc values

      // Reading mag data but not currently publishing it
      
      // Calculate the magnetometer values in milliGauss
      // Include factory calibration per data sheet and user environmental corrections
      // Get actual magnetometer value, this depends on scale being set      
      myIMU.mx = (float)myIMU.magCount[0] * myIMU.mRes
                 * myIMU.factoryMagCalibration[0] - myIMU.magBias[0];
      myIMU.my = (float)myIMU.magCount[1] * myIMU.mRes
                 * myIMU.factoryMagCalibration[1] - myIMU.magBias[1];
      myIMU.mz = (float)myIMU.magCount[2] * myIMU.mRes
                 * myIMU.factoryMagCalibration[2] - myIMU.magBias[2];
    }
                               
    // Is it time to publish IMU data
    myIMU.delt_t = millis() - myIMU.count;
    if (myIMU.delt_t > 50)
    {
      // IMU
      imuMsg.header.frame_id = imu_link;
      imuMsg.header.stamp = nh.now();

      // We are not providing orientation so the 
      // first element of the this matrix should be -1
      imuMsg.orientation_covariance[0] = -1;

      imuMsg.angular_velocity.x = myIMU.gx * DEG_TO_RAD;
      imuMsg.angular_velocity.y = myIMU.gy * DEG_TO_RAD;
      imuMsg.angular_velocity.z = myIMU.gz * DEG_TO_RAD;

      // angular velocity covariance
      imuMsg.angular_velocity_covariance[0] = 0.003;
      imuMsg.angular_velocity_covariance[4] = 0.003;
      imuMsg.angular_velocity_covariance[8] = 0.003;

      imuMsg.linear_acceleration.x = myIMU.ax * G_TO_MS2;
      imuMsg.linear_acceleration.y = myIMU.ay * G_TO_MS2;
      imuMsg.linear_acceleration.z = myIMU.az * G_TO_MS2;
      
      // linear acceleration covariance
      imuMsg.linear_acceleration_covariance[0] = 0.1;
      imuMsg.linear_acceleration_covariance[4] = 0.1;
      imuMsg.linear_acceleration_covariance[8] = 0.1;

      imuPub.publish(&imuMsg);

      myIMU.count = millis();
    }    
  }
  
  nh.spinOnce();
}

// Callback for when servo array message received
void servo_cb( const servo_msgs::servo_array& cmd_msg)
{
  /* Which servo to drive */
  switch(cmd_msg.index)
  {
    case 0:      
      servo0.write(cmd_msg.angle); //set servo 0 angle, should be from 0-180
      break;

    case 1:
      servo1.write(cmd_msg.angle); //set servo 1 angle, should be from 0-180
      break;

    case 2:
      servo2.write(cmd_msg.angle); //set servo 2 angle, should be from 0-180
      break;

    case 3:
      servo3.write(cmd_msg.angle); //set servo 3 angle, should be from 0-180
      break;
      
    default:
      nh.logdebug("Error incorrect servo index");
      break;
  }
}

// ISR
void WheelSpeed0()
{
  int state = digitalRead(ENCODER0_PINA);

  if((encoder0PinALast == LOW) && (state == HIGH))
  {
    int val = digitalRead(ENCODER0_PINB);

    if(val == LOW && encoder0Direction)
    {
      encoder0Direction = false; // Reverse
    }
    else if (val == HIGH && !encoder0Direction)
    {
      encoder0Direction = true; // Forward
    }
  }

  encoder0PinALast = state;

  if(!encoder0Direction)
  {
    encoder0Count++;
  }
  else
  {
    encoder0Count--;
  }
}

// ISR
void WheelSpeed1()
{
  int state = digitalRead(ENCODER1_PINA);

  if((encoder1PinALast == LOW) && (state == HIGH))
  {
    int val = digitalRead(ENCODER1_PINB);

    if(val == LOW && encoder1Direction)
    {
      encoder1Direction = false; // Reverse
    }
    else if (val == HIGH && !encoder1Direction)
    {
      encoder1Direction = true; // Forward
    }
  }

  encoder1PinALast = state;

  if(!encoder1Direction)
  {
    encoder1Count++;
  }
  else
  {
    encoder1Count--;
  }
}

ros.h

#ifndef _ROS_H_
#define _ROS_H_

// As we are no longer including any .h file in the root of the 
// ros_lib library the IDE can't find ros/node_handle.h
// Add the dummy.h empty file in the ros_lib root
#include <dummy.h>
#include <ros/node_handle.h>
#include "ArduinoHardware.h"

namespace ros
{
  
#if defined(__MK64FX512__) || defined(__MK66FX1M0__)
  // Teensy 3.5 or 6.3
  typedef NodeHandle_<ArduinoHardware, 25, 25, 512, 512> NodeHandle;
#elif defined(__AVR_ATmega328P__)
  // arduino Nano
  // 10 publishers, 15 subscribers, 128 bytes input buffer and 256 bytes output buffer
  typedef NodeHandle_<ArduinoHardware, 10, 15, 128, 256> NodeHandle;
#else
  typedef NodeHandle_<ArduinoHardware> NodeHandle; // default 25, 25, 512, 512 
#endif 
}

#endif

ArduinoHardware.h

#ifndef ROS_ARDUINO_HARDWARE_H_
#define ROS_ARDUINO_HARDWARE_H_

#if ARDUINO>=100
  #include <Arduino.h>  // Arduino 1.0
#else
  #include <WProgram.h>  // Arduino 0022
#endif

#if defined(__MK20DX128__) || defined(__MK20DX256__) || 
    defined(__MK64FX512__) || defined(__MK66FX1M0__) || defined(__MKL26Z64__)
  #if defined(USE_TEENSY_HW_SERIAL)
    #define SERIAL_CLASS HardwareSerial // Teensy HW Serial
  #else
    #include <usb_serial.h>  // Teensy 3.0 and 3.1
    #define SERIAL_CLASS usb_serial_class
  #endif
#elif defined(_SAM3XA_)
  #include <UARTClass.h>  // Arduino Due
  #define SERIAL_CLASS UARTClass
#elif defined(USE_USBCON)
  // Arduino Leonardo USB Serial Port
  #define SERIAL_CLASS Serial_
#elif (defined(__STM32F1__) and !(defined(USE_STM32_HW_SERIAL))) or defined(SPARK) 
  // Stm32duino Maple mini USB Serial Port
  #define SERIAL_CLASS USBSerial
#else 
  #include <HardwareSerial.h>  // Arduino AVR
  #define SERIAL_CLASS HardwareSerial
#endif

class ArduinoHardware {
  public:
#if defined(__MK64FX512__) || defined(__MK66FX1M0__)  
    ArduinoHardware(SERIAL_CLASS* io , long baud= 500000){
      iostream = io;
      baud_ = baud;
    }
    ArduinoHardware()
    {
#if defined(USBCON) and !(defined(USE_USBCON))
      /* Leonardo support */
      iostream = &Serial1;
#elif defined(USE_TEENSY_HW_SERIAL) or defined(USE_STM32_HW_SERIAL)
      iostream = &Serial1;
#else
      iostream = &Serial;
#endif
      baud_ = 500000;
    }
#else // Not a Teensy
    ArduinoHardware(SERIAL_CLASS* io , long baud= 57600){
      iostream = io;
      baud_ = baud;
    }
    ArduinoHardware()
    {
#if defined(USBCON) and !(defined(USE_USBCON))
      /* Leonardo support */
      iostream = &Serial1;
#elif defined(USE_TEENSY_HW_SERIAL) or defined(USE_STM32_HW_SERIAL)
      iostream = &Serial1;
#else
      iostream = &Serial;
#endif
      baud_ = 57600;
    }
#endif // defined(__MK64FX512__) || defined(__MK66FX1M0__)  
    ArduinoHardware(ArduinoHardware& h){
      this->iostream = h.iostream;
      this->baud_ = h.baud_;
    }
  
    void setBaud(long baud){
      this->baud_= baud;
    }
  
    int getBaud(){return baud_;}

    void init(){
#if defined(USE_USBCON)
      // Startup delay as a fail-safe to upload a new sketch
      delay(3000); 
#endif
      iostream->begin(baud_);
    }

    int read(){return iostream->read();};
    void write(uint8_t* data, int length){
      for(int i=0; i<length; i++)
        iostream->write(data[i]);
    }

    unsigned long time(){return millis();}

  protected:
    SERIAL_CLASS* iostream;
    long baud_;
};

#endif

串行节点波特率

ArduinoHardware.h 文件中,我们将串口的波特率更改为 500000 波特。当启动 ROS 串行节点时,我们需要在 Raspberry Pi 端反映此更改。我对 rodney.launch 文件进行了另一项更改以设置波特率。

 !-- Teensy. 
     Use the defaults /dev/ttyACM0 (or teensy if dev rules updated) and 500000 -->
<arg name="serial_port" default="/dev/teensy"/>
<arg name="baud_rate" default="500000"/>  
<node pkg="rosserial_python" type="serial_node.py" name="serial_node" output="screen">
  <param name="port" value="$(arg serial_port)"/>
  <param name="baud" value="$(arg baud_rate)"/>
</node>

IMU 校准

当我们设置 IMU 时,它包括加载一些出厂校准值,但我们可以通过包含额外的校准来改进 IMU 数据。

为此,我们将使用一个可从 这个 GitHub 站点获取的 ROS 包。我们还将分支该存储库并自行进行一些代码更改。未更改的包包含两个节点。其中第一个计算加速度计校准参数并将它们保存到 YAML 文件中。我们只需要运行一次,并且由于我们必须将 IMU 以相当精确的定位放置在六个方向上,因此最好在将 IMU 安装到机器人之前完成此操作。第二个节点使用第一个节点创建的校准文件将校准应用于未校准的 IMU 主题以生成校准后的 IMU 主题。第二个节点还可以选择在启动时计算陀螺仪偏差,然后将这些值从原始数据中减去。尽管草图内部的 IMU 设置也执行此操作,但我发现如果启用此选项,我获得了更好的结果。

此外,我将对第二个节点进行更改。尽管我们已经校准了 IMU,但一旦安装到机器人上,它可能不会完全垂直于地面。这将导致加速度计漂移。现在,由于我只期望在家里使用 Rodney,并且不期望他攀爬或下降任何斜坡,我们也可以抵消这个偏移。

为了进行一次性校准并使 IMU 沿着其每个轴达到相当准确的位置,我将其安装到我 3D 打印的校准立方体中。尽管该立方体是用于不同型号的 IMU,但稍加填充,我成功安装了我的 IMU。

图片显示了安装在校准立方体中的 IMU。IMU 由一个备用 Teensy 控制,但您可以使用安装在机器人中的那个。

校准节点 do_calib 期望 IMU 数据在主题 imu 中广播。为了启动 ROS 串行节点并重新映射主题,我创建了一个启动文件 cal_imu.launch,它位于 rodney/launch 文件夹中。

<?xml version="1.0" ?>
<launch> 

  <!-- Teensy. 
       Use the defaults /dev/ttyACM0 (or teensy if dev rules updated) and 500000 -->
  <arg name="serial_port" default="/dev/ttyACM0"/>
  <arg name="baud_rate" default="500000"/>  
  <node pkg="rosserial_python" type="serial_node.py" 
  name="serial_node" output="screen">
    <param name="port" value="$(arg serial_port)"/>
    <param name="baud" value="$(arg baud_rate)"/>
    <remap from="/imu/data_raw" to="imu"/>
  </node>
  
  <!-- now "rosrun imu_calib do_calib" in a shell -->

</launch>

串行节点运行后,在另一个终端中,输入以下命令:

$ rosrun imu_calib do_calib

然后按照终端上给出的说明进行操作。生成校准文件后,将其复制到 rodney/config 文件夹。

我对 imu_calib 包所做的更改仅限于 apply_calib.cpp.h 文件。

在构造函数中,我添加了从参数服务器读取参数的功能,以便可以启用或禁用此功能。

  nh_private.param<bool>("null_accelerometer", null_accelerometer_, true);
  nh_private.param<int>("null_accelerometer_samples", null_accelerometer_samples_, 100);

在函数 rawImuCallback 中,我添加了代码以在启动时计算平均加速度计偏移量。

  if(null_accelerometer_ == true)
  {
    ROS_INFO_ONCE("Nulling accelerometer; do not move the IMU");
    
    // Recursively compute mean accelerometer measurements from corrected acceleration readings
    sensor_msgs::Imu corrected = *raw;
    accel_sample_count_++;

    calib_.applyCalib(raw->linear_acceleration.x, 
                 raw->linear_acceleration.y, raw->linear_acceleration.z,
                 &corrected.linear_acceleration.x, &corrected.linear_acceleration.y, 
                 &corrected.linear_acceleration.z);
                      
    accel_bias_x_ = ((accel_sample_count_ - 1) * 
                      accel_bias_x_ + corrected.linear_acceleration.x) / accel_sample_count_;    
    accel_bias_y_ = ((accel_sample_count_ - 1) * 
                      accel_bias_y_ + corrected.linear_acceleration.y) / accel_sample_count_;
    accel_bias_z_ = ((accel_sample_count_ - 1) * accel_bias_z_ + 
                     (corrected.linear_acceleration.z-9.80665)) / accel_sample_count_;

    if (accel_sample_count_ >= null_accelerometer_samples_)
    {
      ROS_INFO("Nulling accelerometers complete! (bias = [%.3f, %.3f, %.3f])", 
                accel_bias_x_, accel_bias_y_, accel_bias_z_);
      null_accelerometer_ = false;
    }    
  }

然后将这些值从校准后的值中减去。

    corrected.linear_acceleration.x -= accel_bias_x_;
    corrected.linear_acceleration.y -= accel_bias_y_;
    corrected.linear_acceleration.z -= accel_bias_z_;

更新后的软件包可在代码 zip 文件中的 imu_calib 文件夹下找到。

以下内容已添加到 rodney.launch 文件中,以启动更新的节点并传递校准文件的位置。

<!-- Add calibration to raw imu data -->
<node pkg="imu_calib" type="apply_calib" name="imu_calib" output="screen">
  <param name="calib_file" value="$(find rodney)/config/imu_calib.yaml"/>
</node>

在本文的上一部分,我们添加了 robot_localization 包中的 ekf_localization_node。该节点将融合 IMU 数据与原始里程计数据,以生成里程计数据,该数据将用于我将在下一篇文章中介绍的导航系统。

此数据融合的配置包含在 robot_localization.yaml 文件中,该文件位于 rodney/config 文件夹中。我们曾在上一篇文章中简要讨论过此文件,但在此处添加一些详细信息是值得的。

配置矩阵中的数据表示以下内容:

[ x position,     y position,     z position,
  roll,           pitch,          yaw,
  x velocity,     y velocity,     z velocity,
  roll velocity,  pitch velocity, yaw velocity,
  x acceleration, y acceleration, z, acceleration]

矩阵中的“true”值表示该位置的数据将由扩展卡尔曼滤波器使用。

现在对于原始里程计数据,我有:

odom0_config: [false, false, false,
               false, false, false,
               true,  true,  false,
               false, false, true,               
               false, false, false]

虽然里程计消息也包含位置数据,但它与速度数据来源于同一来源,因此没有必要两次使用这些数据。也可以说,既然我们总是将 Y 轴速度设置为零,我们就可以将这个矩阵值设置为 false。但是,如果您想从 IMU 中包含 Y 轴速度,那么通过在里程计矩阵中包含此参数,有助于抵消 IMU 的漂移。

现在,对于 IMU 矩阵,我尝试了两种不同的配置。首先是一种只使用 IMU 数据中的偏航速度的配置。

imu0_config: [false, false, false,
              false, false, false,
              false, false, false,
              false, false, true,
              false, false, false]

您也可以尝试使用 x 和 y 速度的配置,但您需要确保首先减少数据中的任何噪声/误差,否则里程计将受到 x 和/或 y 轴漂移的影响。

imu0_config: [false, false, false,
              false, false, false,
              true, true, false,
              false, false, true,
              false, false, false]

静态变换

在上一部分中,我们引入了机器人底部相对于地面的高度的静态变换。此闩锁变换广播是通过 rodney.launch 文件使用以下内容设置的:

<node pkg="tf2_ros" type="static_transform_publisher" 
      name="base_footprint_broadcaster" args="0 0 0.09 0 0 0 /base_footprint /base_link"/>

现在我们已经添加了 LIDAR 和 IMU,我们需要添加更多的静态变换。计算是从机器人中心进行的,因此扫描消息中报告的任何距离都需要从 LIDAR 的位置转换为机器人中心。

我们可以像上面那样在启动文件中添加两个更多的静态变换,但我们将删除现有的一个并编写一个简单的节点,我们将把它包含在 rodney 包中,以处理所有三个静态变换。

rodney/src 文件夹中,我添加了 static_broadcaster.py 文件。这个 Python 脚本将创建一个负责广播三个静态变换的节点。这个消息是闩锁的,因此需要变换的节点将在启动时获得数据。

# Rodney robot static transform broadcaster

import sys
import rospy
import tf
import tf2_ros
import geometry_msgs.msg

def main(args):
    rospy.init_node('rodney_static_broadcaster', anonymous=False)
    rospy.loginfo("rodney static broadcaster node started")
    
    broadcaster = tf2_ros.StaticTransformBroadcaster()

    # Static transform for the base_footprint to base_link
    st1 = geometry_msgs.msg.TransformStamped()
    st1.header.stamp = rospy.Time.now()
    st1.header.frame_id = "/base_footprint"
    st1.child_frame_id = "/base_link"
    st1.transform.translation.x = 0.0
    st1.transform.translation.y = 0.0
    st1.transform.translation.z = 0.09
    quat = tf.transformations.quaternion_from_euler(0.0, 0.0, 0.0)
    st1.transform.rotation.x = quat[0]
    st1.transform.rotation.y = quat[1]
    st1.transform.rotation.z = quat[2]
    st1.transform.rotation.w = quat[3]
    
    # Static transform for the baselink to laser
    st2 = geometry_msgs.msg.TransformStamped()
    st2.header.stamp = rospy.Time.now()
    st2.header.frame_id = "/base_link"
    st2.child_frame_id = "/laser"
    st2.transform.translation.x = 0.085
    st2.transform.translation.y = 0.0
    st2.transform.translation.z = 0.107
    quat = tf.transformations.quaternion_from_euler(0.0, 0.0, 0.0)
    st2.transform.rotation.x = quat[0]
    st2.transform.rotation.y = quat[1]
    st2.transform.rotation.z = quat[2]
    st2.transform.rotation.w = quat[3]

    # Static transform for the baselink to imu
    st3 = geometry_msgs.msg.TransformStamped()
    st3.header.stamp = rospy.Time.now()
    st3.header.frame_id = "/base_link"
    st3.child_frame_id = "/imu"
    st3.transform.translation.x = 0.0
    st3.transform.translation.y = 0.0
    st3.transform.translation.z = 0.058
    quat = tf.transformations.quaternion_from_euler(0.0, 0.0, 0.0)
    st3.transform.rotation.x = quat[0]
    st3.transform.rotation.y = quat[1]
    st3.transform.rotation.z = quat[2]
    st3.transform.rotation.w = quat[3]
         
    broadcaster.sendTransform([st1, st2, st3]) 
    
    # static transforms are latched so all we need to do here is spin
    rospy.spin()     

if __name__ == '__main__':
    main(sys.argv)

该节点在 rodney.launch 文件中启动

<!-- Static transforms in the system -->
<node pkg="rodney" type="static_broadcaster.py" name="static_broadcaster_node"/>

启动文件

我对 rodney.launch 文件做的另一个更改是删除了...

output="screen"

...从不再处于开发或测试阶段的每个节点中。这减少了我们启动机器人代码的终端中显示的日志消息。但是,您仍然可以在远程终端上使用 rqt_console 来监视所有日志消息。

完整的 rodney.launch 文件现在看起来像这样:

<?xml version="1.0" ?>
<launch>    
  <!-- Static transforms in the system -->
  <node pkg="rodney" type="static_broadcaster.py" name="static_broadcaster_node"/>
  
  <!-- Load each of the config files into the parameter server -->           
  <rosparam command="load" file="$(find pan_tilt)/config/config.yaml"/>
  <rosparam command="load" file="$(find face_recognition)/config/config.yaml"/>
  <rosparam command="load" file="$(find head_control)/config/config.yaml"/>
  <rosparam command="load" file="$(find rodney_missions)/config/config.yaml"/>  
    
  <!-- Launch the camera node from one of its launch files -->
  <include file="$(find raspicam_node)/launch/camerav2_1280x960.launch" /> 
  
  <!-- Start all the nodes that make up Rondey -->
  <!-- Starting with those written for the project -->
  <node pkg="pan_tilt" type="pan_tilt_node" name="pan_tilt_node"/>
  <node pkg="face_recognition" type="face_recognition_node.py" name="face_recognition_node"/>
  <node pkg="head_control" type="head_control_node" name="head_control_node"/>  
  <node pkg="speech" type="speech_node" name="speech_node"/>
  <node pkg="rodney_missions" type="rodney_missions_node.py" 
        name="rodney_missions" output="screen"/>
  <node pkg="rodney" type="rodney_node" name="rodney" output="screen">
    <rosparam command="load" file="$(find rodney)/config/config.yaml"/> 
  </node>
  <node pkg="thunderborg" type="thunderborg_node.py" name="thunderborg_node">
    <rosparam command="load" file="$(find thunderborg)/config/config.yaml"/>
  </node>
  
  <!-- Teensy. 
       Use the defaults /dev/ttyACM0 (or teensy if dev rules updated) and 500000 -->
  <arg name="serial_port" default="/dev/teensy"/>
  <arg name="baud_rate" default="500000"/>  
  <node pkg="rosserial_python" type="serial_node.py" name="serial_node" output="screen">
    <param name="port" value="$(arg serial_port)"/>
    <param name="baud" value="$(arg baud_rate)"/>
  </node>
  
  <!-- The RPLidar and laser filter node 
       Have created symbolic link for /dev/ttyUSBn to be rplidar -->
  <node pkg="rplidar_ros" type="rplidarNode" name="rplidar_node" output="screen">
    <param name="serial_port" type="string" value="/dev/rplidar"/>
    <param name="serial_baudrate" type="int" value="115200"/>
    <param name="frame_id" type="string" value="laser"/>
    <remap from="scan" to="scan_filter_input"/>
  </node>
  <node pkg ="laser_filters" type="scan_to_scan_filter_chain" 
        name="scan_to_scan_filter_chain" output="screen">
    <rosparam command="load" file="$(find rodney)/config/laser_filter_config.yaml"/>
    <remap from="scan" to="scan_filter_input"/>
    <remap from="scan_filtered" to="scan"/>
  </node>
  
  <!-- The robot face -->
  <node pkg="homer_robot_face" type="RobotFace" name="RobotFace"/>
  
  <!-- Add calibration to raw imu data -->
  <node pkg="imu_calib" type="apply_calib" name="imu_calib" output="screen">
    <param name="calib_file" value="$(find rodney)/config/imu_calib.yaml"/>
  </node>
    
  <!-- Node to fuse motor encoder and IMU data for odom -->
  <node pkg="robot_localization" type="ekf_localization_node" name="ekf_localization_node">
    <remap from="odometry/filtered" to="odom"/>
    <rosparam command="load" file="$(find rodney)/config/robot_localization.yaml"/> 
  </node>
   
</launch>

微小代码更改

我还对代码进行了一些小的更改,主要是为了能够在机器人处于手动模式时启用/禁用 LIDAR,并确保在远程控制时机器人不会失控。

rodney_missions 节点更新

我做了一些小改动,以便该节点可以通过启动和停止 LIDAR 电机来启用/禁用 LIDAR。请求来自 rodney 节点,但在这里进行控制很重要,这样该节点才能确保在将来请求自主导航时 LIDAR 正在运行。

电机的启动和停止是通过 ROS 服务调用 LIDAR 节点来完成的。第一个更改在 MissionsHelper 类的 __init__ 函数中。我们等待服务可用,创建代理调用以便我们可以访问服务,然后调用 LidarEnable 辅助函数以确保 LIDAR 当前正在运行。

# RPLidar services to start and stop the motor
rospy.wait_for_service('stop_motor')
rospy.wait_for_service('start_motor')
self.__rplidar_stop_motor_srv = rospy.ServiceProxy('stop_motor', std_srvs.srv.Empty)
self.__rplidar_start_motor_srv = rospy.ServiceProxy('start_motor', std_srvs.srv.Empty)
# LIDAR should be running but make sure
self.LidarEnable() 

接下来,我向 MissionHelper 类添加了三个辅助函数。它们调用相关的服务并跟踪 LIDAR 电机的当前状态。

# Function to enable the RPLidar
def LidarEnable(self):        
    self.__rplidar_start_motor_srv()
    self.__lidar_on = True
        
# Function to disable the RPLidar
def LidarDisable(self):        
    self.__rplidar_stop_motor_srv() 
    self.__lidar_on = False 
        
# Function to Toggle RPLidar on/off
def ToggleLidar(self):
    if(self.__lidar_on == True):
        self.LidarDisable()
    else:
        self.LidarEnable() 

最后的更改在 Prepare 类中,并向 execute 函数添加了一个新的 elif 构造。如果收到的作业 ID 为 'J4',则请求切换 LIDAR 的当前状态。

elif parameters[0] == 'J4':
    # Simple job to toggle the LIDAR on/off
    self.__helper_obj.ToggleLidar()

rodney 包更新

我们需要对 rodney 节点进行一些更改,以便我们可以在手动模式下使用连接到远程工作站的操纵杆和/或键盘来启用/禁用 LIDAR。

此外,在我手动控制机器人时,我的家庭网络偶尔会中断几秒钟。这里的问题是机器人将继续使用从上次操纵杆或键盘输入创建的速度。因此,我在 rodney 包中添加了一个新节点(remote_heartbeat_node),该节点在远程工作站上运行并发布心跳消息。在机器人硬件上运行的 rodney 节点监视此消息,如果机器人在手动模式下并且一秒钟内未收到消息,则速度将设置为零。

rodney_node.cpp 文件的更改。

我们现在可以从操纵杆启用/禁用 LIDAR,因此我们需要一种方法来配置哪个按钮控制此功能。将以下行添加到 RodneyNode 构造函数中。

nh_.param("/controller/buttons/lidar_enable", lidar_enable_select_, 2);

joystickCallback 函数中,添加以下“if”构造以请求在按下相关操纵杆按钮时切换 LIDAR 电机状态。

// Button on controller selects to enable/disable the lidar function
if((manual_locomotion_mode_ == true) && (msg->buttons[lidar_enable_select_] == 1))
{
    std_msgs::String mission_msg;
        
    // Toggle the LIDAR on/off
    mission_msg.data = "J4";            
        
    mission_pub_.publish(mission_msg);        
    last_interaction_time_ = ros::Time::now();
}

同样,如果按下键盘上的“l”键,则电机状态将被切换。将以下“else if”构造添加到 keyboardCallBack 函数中。

else if((msg->code == keyboard::Key::KEY_l) && 
       ((msg->modifiers & ~RodneyNode::SHIFT_CAPS_NUM_LOCK_) == 0))
{
    if(manual_locomotion_mode_ == true)
    {
        std_msgs::String mission_msg;
        
        // Toggle the LIDAR on/off
        mission_msg.data = "J4";            
        
        mission_pub_.publish(mission_msg);        
        last_interaction_time_ = ros::Time::now();
    }
}      

接下来,节点需要通过将以下内容添加到 RodneyNode 构造函数来订阅新的 heartbeat 消息。

remote_heartbeat_sub_ = nh_.subscribe
                        ("remote_heartbeat", 1, &RodneyNode::remHeartbeatCallback, this);

当接收到此主题上的消息时,将有一个回调函数来存储最后一条消息的时间。

// Callback for remote heartbeat
void RodneyNode::remHeartbeatCallback(const std_msgs::Empty::ConstPtr& msg)
{
    // Remote heartbeat received store the time
    remote_heartbeat_time_ = ros::Time::now();
}

sendTwist 函数中,如果自上次心跳消息收到已超过一秒,我们需要将速度设置为零。下面是 sendTwist 函数的完整新版本。

void RodneyNode::sendTwist(void)
{
    geometry_msgs::Twist target_twist;
    
    // If in manual locomotion mode use keyboard or joystick data
    if(manual_locomotion_mode_ == true)
    {
        // Only allow stored keyboard or joystick values to set  
        // the velocities if the remote heartbeat is running
        if((ros::Time::now() - remote_heartbeat_time_).toSec() < 1.0)
        {        
            // Publish message based on keyboard or joystick speeds
            if((keyboard_linear_speed_ == 0) && (keyboard_angular_speed_ == 0))
            {
                // Use joystick values
                target_twist.linear.x = joystick_linear_speed_;
                target_twist.angular.z = joystick_angular_speed_;            
            }
            else
            {
                // use keyboard values
                target_twist.linear.x = keyboard_linear_speed_;
                target_twist.angular.z = keyboard_angular_speed_;                   
            }
        }
        else
        {
            // Lost connection with remote workstation so zero the velocities
            target_twist.linear.x = 0.0;
            target_twist.angular.z = 0.0; 
        }
    }
    else
    {
        // Use mission demands (autonomous)
        target_twist.linear.x = linear_mission_demand_;
        target_twist.angular.z = angular_mission_demand_;
    }

    // If not using the PID ramp to the target value. 
    if (false == pid_enabled_)
    {
        ros::Time time_now = ros::Time::now();
        
        // Ramp towards are required twist velocities
        last_twist_ = rampedTwist(last_twist_, target_twist, last_twist_send_time_, time_now);
        
        last_twist_send_time_ = time_now;
        
        // Publish the Twist message using the ramp value
        twist_pub_.publish(last_twist_);
    }
    else
    {
        // Publish the Twist message using the target value
        twist_pub_.publish(target_twist);
    }        
}

接下来,我们将 remote_heartbeat_node.cpp 文件添加到 rodney/src 文件夹中。请记住,此节点不会在机器人硬件上运行,而是在用于手动驾驶机器人的远程工作站上运行。这个简单的节点只是以 5Hz 的频率广播心跳消息。

// This heartbeat node is not to be run on the robot platform but on a remote worksation
// when either the keyboard or joystick nodes are being used to teleop the robot. If the
// message sent by this node is missed for 1 second, the robot will stop using the keyboard
// and joystick stored values to drive the motors.
#include <ros/ros.h>
#include <std_msgs/Empty.h>

int main(int argc, char **argv)
{   
    ros::init(argc, argv, "remote_heartbeat");
    ros::NodeHandle n;    
    
    ros::Publisher remote_heartbeat_pub = n.advertise<std_msgs::Empty>("remote_heartbeat", 1);
       
    std::string node_name = ros::this_node::getName();
    ROS_INFO("%s started", node_name.c_str());
	
    ros::Rate r(5); // 5Hz	
    
    std_msgs::Empty beat;    
    
    while(ros::ok())
    {
        remote_heartbeat_pub.publish(beat);
        
        ros::spinOnce();
        r.sleep();
    }
    
    return 0;    
}

rodney 包的最后一次更改是添加一个启动文件,该文件可用于同时启动键盘、操纵杆和心跳节点。文件名是 remote.launch,它位于 rodney/launch 文件夹中。

<?xml version="1.0" ?>
<launch> 
  <node pkg="joystick" type="joystick_node" name="joystick_node"/>
  <node pkg="keyboard" type="keyboard" name="keyboard"/>
  <node pkg="rodney" type="remote_heartbeat_node" name="remote_heartbeat_node" output="screen"/>
</launch>

机器人硬件

当前电路图的全尺寸图像可在图表 zip 文件夹中找到,以及显示所有节点和主题的 rqt_graph 图像的全尺寸副本。

到目前为止,该项目的完整物料清单可在此处获取

在本文的第 1 部分中,我提到了我在树莓派上使用的 Ubiquity 机器人镜像。有关如何安装镜像、安装额外软件和配置它以用于项目的说明可在此处获取

Using the Code

像往常一样,我将在机器人硬件上运行代码,并在Linux PC上运行测试工具和手动控制节点。在下面的细节中,我将这台PC称为工作站。

在Pi上构建ROS包(机器人硬件)

如果尚未完成,请在 Raspberry Pi 上创建一个 catkin 工作区,并使用以下命令初始化它

$ mkdir -p ~/rodney_ws/src
$ cd ~/rodney_ws/
$ catkin_make

face_recognitionface_recognition_msgshead_controlimu_calibpan_tiltrodneyrodney_missionsros-keyboardrplidar-rosservo_msgsspeechtacho_msgsthunderborg 包复制到 ~/rodney_ws/src 文件夹中。

使用以下命令构建代码

$ cd ~/rodney_ws/ 
$ catkin_make

检查构建是否无任何错误地完成。

您还需要编译并将草图下载到 Teensy 3.5。

在工作站上构建ROS包

在工作站上,我们希望运行键盘、操纵杆和心跳节点,以便能够远程控制实际的机器人硬件。

使用以下命令创建一个工作空间

$ mkdir -p ~/test_ws/src 
$ cd ~/test_ws/ 
$ catkin_make

rodneyjoystickodom_testros-keyboard 包复制到 ~/test_ws/src 文件夹中,然后使用以下命令构建代码:

$ cd ~/test_ws/ 
$ catkin_make

检查构建是否无任何错误地完成。

提示

在工作站和 Raspberry Pi 上运行 ROS 代码和工具时,可能需要在多个终端上重复输入大量命令。在接下来的部分中,我将包含完整的输入命令,但这里有一些技巧可以为您节省所有这些输入。

在 Raspberry Pi 上,为了节省输入 "source devel/setup.bash",我已将其添加到 Raspberry Pi 的 .bashrc 文件中。

$ cd ~/
$ nano .bashrc

然后将 "source /home/ubuntu/rodney_ws/devel/setup.bash" 添加到文件末尾,保存并退出。

在工作站上运行测试代码和工具时,它也需要知道 ROS master 的位置,所以我已经将以下内容添加到工作站的 .bashrc 文件中。

alias rodney='source ~/test_ws/devel/setup.bash; \
export ROS_MASTER_URI=http://ubiquityrobot:11311'

然后只需在终端输入 "rodney",这两个命令就会运行,省去了大量的输入。

您还可以节省一些输入,因为某些ROS工具支持TAB补全。例如,键入rosrun rosserial_,然后按Tab键自动补全rosrun rosserial_python

开始编码

在机器人硬件上,运行以下命令启动系统中所有当前节点:

$ source rodney_ws/devel/setup.bash
$ roslaunch rodney rodney.launch

在工作站上,运行以下命令启动远程控制节点:

$ source test_ws/devel/setup.bash 
$ export ROS_MASTER_URI=http://ubiquityrobot:11311 
$ roslaunch rodney remote.launch

一个标题为“ROS 键盘输入”的小窗口应该正在运行。输入键盘按键时,请确保小窗口处于焦点。

在工作站上的另一个终端中,使用以下命令启动 rviz

$ source test_ws/devel/setup.bash 
$ export ROS_MASTER_URI=http://ubiquityrobot:11311 
$ roslaunch rodney rviz.launch

测试 LIDAR

配置 rviz,使其固定帧为 base_link,并使 LaserScan 显示 /scan 主题。您应该会在 rviz 中看到扫描消息的可视化表示。

还应该可以使用操纵杆控制器或键盘远程控制机器人,以避开激光器检测到的障碍物。

在工作站的一个新终端中,输入以下命令禁用 LIDAR:

$ source test_ws/devel/setup.bash 
$ export ROS_MASTER_URI=http://ubiquityrobot:11311
$ rosservice call /stop_motor

LIDAR 应该停止旋转,并且 rviz 中的扫描显示不应更新。

在终端中,输入以下命令启用 LIDAR:

$ rosservice call /start_motor

LIDAR 应该开始旋转,并且 rviz 中的扫描显示应该会更新。

测试 IMU

为了确保原始和融合里程计均为零,请关闭启动代码的机器人硬件上的终端。

在机器人硬件上,在一个新终端中,使用以下命令重新启动:

$ source rodney_ws/devel/setup.bash
$ roslaunch rodney rodney.launch

配置 rviz,以便:

  • 固定帧为 odom
  • 显示变换 (TF) base_linkodom
  • 里程计 /raw_odom/odom 以不同颜色显示

标记机器人在地板上的位置。使用键盘或操纵杆移动机器人。

在工作站上的一个终端中,输入以下内容以查看当前里程计消息:

$ source test_ws/devel/setup.bash
$ export ROS_MASTER_URI=http://ubiquityrobot:11311
$ rostopic echo /odom

从消息中,记下当前姿态位置 xy。测量机器人从标记位置移动的距离,并确认它是一个很好的近似值。

使用键盘或操纵杆旋转机器人。姿态方向不容易比较,因为它是一个四元数。您可以在 rviz 中看到方向,但您也可以使用测试脚本 odom_test 来确认角度(以度为单位)。

在工作站的终端中,按 Ctrl-C,然后输入以下命令:

$ rosrun odom_test odom_test1_node.py

每秒一次,节点将以度数打印出原始和融合里程计消息的姿态方向。

rviz 显示中,您将看到两组箭头,显示原始和融合里程计。此外,当机器人返回到其起始位置时,base_linkodom 变换应该非常接近。

图片显示了原始里程计(红色箭头)和融合里程计(绿色箭头)的轨迹

关注点

在这一部分中,我们完成了设计目标 4,并添加了 IMU 以改进里程计。

在下一篇文章中,我们将添加使用 LIDAR 进行自主导航的包,包括从 LIDAR 和里程计变换数据创建地图。我们将使用 rviz 设置目标姿态,机器人将自主导航到设定的位置。

历史

  • 2019/04/15:首次发布
© . All rights reserved.