Lessons learned from QUAIL

Keywords: #ros2 #quail

QUAIL is completed!

Well, it took WAY longer than expected. Life got very busy for me over the last few months, and I struggled to find the time and motivation to get this thing over the line. The last requirement for the project was to ‘close the loop’, and figure out a nice way to feedback the state of the sensor bar. Thankfully, it was all pretty simple once I got going again. You can find the source code Here.

With that, the final structure of the software looks like so:

      ,-------------------------------.
      |         QuailBase             |
      |-------------------------------|
      |Main()                         |
      |--ControllerNode.on_configure()|
      |--ControllerNode.on_activate() |
      |spin()                         |
      `-------------------------------'
                      |
                      V
      ,-------------------------------.
      |       ControllerNode          |
      |-------------------------------|      ,---------------------.
      |on_configure()                 |      |    MotorDriver      |
      |--MotorDriver.configure()      | -->  |---------------------|
      |--SensorNode.SensorNode()      |      |configure()          |
      |on_activate()                  |      |drive_motors_direct()|
      |on_deactivate()                |      |shutdown_driver()    |
      |on_cleanup()                   |      `---------------------'
      |on_shutdown()                  |      ,---------------------------.
      |--MotorDriver.shutdown_driver()|      |     SensorNode            |
      |sensor_callback()              | -->  |---------------------------|
      |-- Subscriber [SensorNode]     |      |timer_callback()           |
      |control_loop()                 |      |-- Publisher (sensor data) |
      `-------------------------------'      `---------------------------'

There isn’t anything too insane going on here. The new SensorNode consists of a publisher, and a timer linked to a callback. The callback polls the current state of the GPIO’s attached to the sensor bar, and publishes a message containing the state (1 or 0) of the GPIO pins. The ControllerNode Subscribes to this publisher, then commands the MotorDriver based on the most recently recieved readings.

Reading the light bar

The light bar worked exactly as I expected (hoped) it would, giving a nice 3v digital output. I could read the state of the pins extremely easily with the pigpiod library:

    const int pin_1_val = gpio_read(pi_, sensor_pin_1_);

With this, we can ‘close the loop’, and give QUAIL the ability to follow a line.

Translating the bar message

BarState.msg
# Readings from the light sensor bar
# Front of robot facing upwards:
# [0] LHS sensor [1] Center sensor [2] RHS sensor
int8[3] reading
sensor_node.cpp
  auto message = quail_msgs::msg::BarState();
  message.reading.at(0) = pin_1_val;
  message.reading.at(1) = pin_2_val;
  message.reading.at(2) = pin_3_val;
  publisher_->publish(message);
controller_node.hpp
  // Straight forward case for bar readings
  std::array<int8_t, 3> straight_ahead_arr_;

  // Slight turn cases
  std::array<int8_t, 3> slight_left_arr_;
  std::array<int8_t, 3> slight_right_arr_;
controller_node.cpp

Init

  straight_ahead_arr_ = {0, 1, 0};
  slight_left_arr_ = {1, 1, 0};
  slight_right_arr_ = {0, 1, 1};

Loop

  // [o x o] Straight forward case
  if (current_bar_reading_.reading == straight_ahead_arr_) {
    motor_driver_.drive_motors_direct(1, 1);
  }
  // [x x o] / [o x x] Slight turn case
  else if (current_bar_reading_.reading == slight_left_arr_) {
    motor_driver_.drive_motors_direct(1 - p_gain_1_, 1);
  } else if (current_bar_reading_.reading == slight_right_arr_) {
    motor_driver_.drive_motors_direct(1, 1 - p_gain_1_);
  }

I tried to make reading/comparing the bar readings as simple as possible, I’m not really sure I could simplify it much more than this. The nice thing about encoding the bar state as an array is that it allows the if/else if block to be essentially self documenting. There are also checks for a ‘hard turn’ ([x 0 0]/[0 0 x]) and ‘panic’ ([x x x], [0 0 0]) cases, but I’ve left them out of the above code blocks for the sake of brevity.

Lessons learned

The Workflow

Around the same time I started this project, I had sshfs on my mind (No idea why). Given how often you need to make changes to code on a robot, I was interested in testing out how much I could streamline the workflow. So, I enforced a soft constraint that I would write develop as much as possible on the raspberry pi via ssh, instead of locally on my desktop. It got extremely old, extremely quickly, but I’m actually happy I went through with it. About half way though, I got sick of not having any kind of completion/checks/navigation tools when using sshfs, so I did some searching to see if we could do better.

Introducing Tramp Mode. This worked extremely well, I was even able to set up an LSP server which ran on the Pi itself, which made writing code feel identical to developing locally. The only real issue was that clangd is extremely hungry on memory, which sometimes caused the pi3 with its lowly 1gb to start swapping. Resulting in me having to kill the clangd server if I wanted to compile after making a number of large changes, or potentially risk the pi dropping the ssh connection for a while.

The silent treatment: Updating

There was a ton of times where I’d boot up the pi, and it would simply not respond to my attempts to ssh into it. By chance, I managed to connect to it one morning and check the processes in an attempt to figure it out. Turns out, Ubuntu Server will automatically run unattended upgrades when it can. It never took too long, but it definitely slowed me down a number of times.

The silent treatment: Resistance

I’ve always been quite open that one of my weakest links is the electrical side of robotics, I just don’t really get to experiment with it enough. Well, I learned a pretty big lesson with QUAIL: Resistance really matters. As it turns out, when you’re using a breadboard, with the cheapest wires you can buy, the resistance adds up VERY quickly. This can also cause the Pi to not be able to draw enough current to boot (Or just crash when under heavy load, like compiling code). Once I realised this, the fix wasn’t too difficult, but it was a bit frustrating to have wasted so much time trying to figure out why the Pi was dropping out. My first guess was the voltage regulator was having issues with the load, or potentially an issue with the battery itself.

What’s next?

Onto the next robot! I’m not going to spoil anything just yet, but I am sure you’re going to love it. I’ve spent a lot of time mapping out the project, and building a better model for translating the each stage into a format suited to writing about. I am pretty confident I should be able to write more interesting content, as well as (Hopefully) publish the content more often. I have also been pretty slack with making git commits, I’ve found myself a bit anxious about potentially pushing crappy code for the world to see. I have made it a target for the next project that I’ll be better about it (The anxiety, not the crappy code. I’ll be doing that no matter what).

Until next time!