Using Bluetooth Controller with ROS 2 on Jetson Nano

Let’s get started with writing our first ROS 2 node! This particular node will be responsible for publishing commands received from the Bluetooth controller, and we will create a new message type for it using C++. If you’re new to ROS 2 nodes, I highly recommend taking some time to familiarize yourself with the basics. You can find some excellent tutorials on the ROS 2 documentation website that cover topics such as how to write a C++ publisher node and how to define a custom message type. We will also learn how to receive input from the Bluetooth controller.

Setup

For this setup, I’m using an NVIDIA Shield Controller as my Bluetooth controller, but any other standard controller should also do the trick. I’ve connected it to my Jetson Nano on the DonkeyJet, and I’m using it in a ROS 2 Docker container we’ve previously created here.

Pairing Bluetooth Controller – Headless

At the moment, my Jetson Nano is located on the DonkeyJet and I haven’t connected a monitor, keyboard, or mouse to it. Before proceeding further, we must pair the Bluetooth device with the Jetson Nano.

Connect to the Jetson using SSH, set the controller in pairing mode, and follow the commands. We are using bluetoothctl for this purpose.

You might notice that there were more devices found during the scan, but I’ve edited the image to remove the extras and make it easier to read. To recap, we started the scan and stopped it as soon as my controller – which is displayed as “NVIDIA Controller” here – appeared. Then, I used the trust command. which make the auto connect possible when controller wakes up from sleep. Then the pair command is used. In some cases, the pair command might also trigger the connect command, so you might not need to run it separately. However, if the connection doesn’t happen automatically, you can execute the connect command separately. It’s important to note that all of these commands are executed with the MAC address of the controller.

In my situation, the device appeared as /dev/input/js0. Once this file exists, I know for sure that the controller is connected. I can also test it with jstest-gtk which can be installed with sudo apt-get install jstest-gtk. But you need to enable the X forwarding of the SSH. In Ubuntu, you just need to use -X argument with your ssh command. The following shows the jstest-gtk window when I am moving the axes and pushing the buttons.

Reading Controller on Jetson Nano

There’s a ROS package named “Joy” that’s supposed to read joystick inputs, but for some reason, it didn’t work with the controller I have. That’s why I’ve decided to create my own node instead.

Reading Joystick Input in Linux with C++

We’ll be using the header file linux/joystick.h for this task, which has a well-written document here explaining how to use it. The original documentation defining the Joystick API can also be found here.

To simplify our work, we can divide it into two stages – the setup stage and the loop stage. During the setup stage, we’ll initialize everything that’s required, and during the loop stage, we’ll read the input events.

In the setup stage, we’ll open the input file – which in this case is /dev/input/js0 – and perform some ioctl calls to retrieve the name of the device, as well as the number of buttons and axes available on the device. It would look something like this:

std::string device{"/dev/input/js0"};
js_fd_ = open(device.c_str(), O_RDONLY | O_NONBLOCK);
if (js_fd_ < 0)
    perror("Could not open joystick");

__u8 number_of_axes;
ioctl(fd, JSIOCGAXES, &axes);
__u8 number_of_buttons;
ioctl(fd, JSIOCGBUTTONS, &buttons)
printf("joystick with %d axes and %d buttons is opened", number_of_axes, number_of_buttons));

In the loop stage, we are continuously reading the file and in case there are any events, we read the events. Something like this:

int read_event(int fd, struct js_event *event)
{
  ssize_t bytes;
  bytes = read(fd, event, sizeof(*event));
  if (bytes == sizeof(*event))
    return 0;
  return errno;
}

void loop(){
  struct js_event event;
  while (read_event(js_fd_, &event) == 0)
  {
    switch (event.type)
    {
    case JS_EVENT_BUTTON:
      printf("Button %u %s", event.number, event.value ? "pressed" : "released");
      break;
    case JS_EVENT_AXIS:
      printf("Axis %u at %6f", event.number, event.value / 32767.0);
      break;
    default:
      /* Ignore init events. */
      break;
    }
  }
}

Joystick with ROS

The next step is to integrate this logic into a ROS 2 node and publish the inputs into a topic. For that purpose, I defined a new message as follows and called it Joy2 since there is a Joy message in Joy package.

int64 INVALID=-2

std_msgs/Header header
int64 button
int64 button_value
int64 axis
float64 axis_value

This file can be found here in my Github repository. The idea is that we use Joy2::INVALID to indicate a field is not valid. Since in each message exclusively only one button or one axis is updated and the other field is invalid.

The complete ROS 2 looks like the following and it also can be found here in my Github repository.

Leave a Comment

Your email address will not be published. Required fields are marked *