Swarmie Recruitment via ROS topics

This is a simple demo of recruitment using the SwarmBaseCode-ROS software for the NASA Swarmathon. Recruitment is used to notify other robots when clusters of cubes have been located to increase the number of robots collecting from a particular pile. This demo ignores many of the finer points of recruitment such as recruiting too many robots or recruiting to two locations simultaneously. A fully fledged recruitment scheme should consider these problems if it is to work well. The basic plan is to broadcast a message with the robot’s current location whenever the robot can see more than one cube. Other robots that receive the message

Recruitment Messages

In order to fit within the rules of the competition recruitment messages will be sent via a ROS topic called /detectionLocations. To keep things clean I will begin by defining a new message type to be used for recruitment.

# swarmie_msgs/msg/Recruitment.msg
float32 x
float32 y
std_msgs/String name

The name of the robot is added so that robots can ignore recruitment messages that they sent themselves. There are more canonical ways to do this in ROS, but they won’t work for us (we will see why later).

We also have to modify the CMakeLists.txt for swarmie_msgs to build the new message type by adding it to the message files list and adding std_msgs as a dependency.

find_package(catkin REQUIRED COMPONENTS 
  message_generation 
  std_msgs
)
# ...
add_message_files(
  FILES
  Waypoint.msg
  Recruitment.msg
)
#...
generate_messages(
  DEPENDENCIES
  std_msgs
)

Publishing our location

To publish our location we will make a new class responsible for deciding when to recruit and for sending the message.

#ifndef _BEHAVIOR_POSITION_PUBLISHER_HPP
#define _BEHAVIOR_POSITION_PUBLISHER_HPP

#include <vector>
#include <string>

#include <ros/ros.h>
#include <std_msgs/String.h>

#include "Point.h"
#include "Tag.h"

class PositionPublisher
{
private:
   ros::Publisher position_publisher;
   std_msgs::String name;

public:
   PositionPublisher(ros::NodeHandle& node_handle, 
                     std::string hostname);
   void setDetections(std::vector<Tag> tags, Point p);
};

#endif // _BEHAVIOR_POSITION_PUBLISHER_HPP

Whenever we get a message from the apriltags package with the current detections we will pass it to an instance of this class along with the current robot’s current location.

void targetHandler(
   const apriltags_ros::AprilTagDetectionArray::ConstPtr& message) {
    /* ... */
    vector<Tag> tags;

    for (int i = 0; i < message->detections.size(); i++) {

      // Package up the ROS AprilTag data into our 
      // own type that does not rely on ROS.
      /* ... */
    }

    Point curr_loc;
    curr_loc.x = currentLocationMap.x;
    curr_loc.y = currentLocationMap.y;
    positionPublisher->setDetections(tags, curr_loc);

    logicController.SetAprilTags(tags);
}

The implementation of the PositionPublisher is very simple. Whenever PositionPublisher::setDetections() is called it counts the number of cube tags it sees (tag id 0) and if that number is greater than 3 it published the current position.

#include "PositionPublisher.hpp"
#include "swarmie_msgs/Recruitment.h"

#include <algorithm> // std::count_if

PositionPublisher::PositionPublisher(ros::NodeHandle& node_handle,
                                     std::string hostname)
{
   position_publisher = 
      node_handle
      .advertise<swarmie_msgs::Recruitment>("/detectionLocations", 1);
   name.data = hostname;
}

void PositionPublisher::setDetections(std::vector<Tag> tags,
                                      Point current_position)
{
   int cube_tag_count =
      std::count_if(tags.begin(), tags.end(),
                    [](Tag t) { return t.getID() == 0; });
   if(cube_tag_count > 3)
   {
      // then there is definitely more than one cube
      swarmie_msgs::Recruitment r;
      r.x = current_position.x;
      r.y = current_position.y;
      r.name = name;
      position_publisher.publish(r);
   }
}

Acting on recruitment

Add a subscriber and handler to ROSAdapter.cpp.

ros::Subscriber recruitmentSubscriber;

void recruitmentHandler(const swarmie_msgs::Recruitment&);

/* ... */
int main(int argc, char **argv) {
   /* ... */
   recruitmentSubscriber = mNH.subscribe("/detectionLocations",
                                         10, recruitmentHandler);
}

The handler will simply tell the logic controller that a recruitment message was received after filtering out messages that were sent by the current robot.

void recruitmentHandler(const swarmie_msgs::Recruitment& msg)
{
   if(msg.name.data != publishedName) {
      Point p;
      p.x = msg.x;
      p.y = msg.y;
      logicController.gotRecruitmentMessage(p);
   }
}

We need to extend the LogicController to act on these messages.

// Remember to add a corresponding declaration in LogicController.h
void LogicController::gotRecruitmentMessage(Point p)
{
   searchController.setRecruitmentLocation(p);
}

In the search controller we will simply override the previous search location whenever a new recruitment message is received (ideally we would want to be more selective about when we should listen to recruitment messages).

// Remember to add a corresponding declaration in SearchController.h
void SearchController::setRecruitmentLocation(Point p)
{
   attemptCount = 1;
   // forget whatever we were doing...
   result.wpts.waypoints.clear();
   // ... and drive to p
   result.wpts.waypoints.insert(result.wpts.waypoints.begin(), p);
}

That’s it! Whenever a robot receives a Recruitment message it attempts to respond by driving towards the indicated location.

Making it work with rosbridge

To make this work when robots are operating independently and communicating with the laptop only through our rosbridge shim you need to add some code to misc/rosbridge/swarmie.js. What this will do is listen for recruitment messages being published from each robot and pass them on to all the other robots. (All of the changes here are in the Swarmie constructor.)

The first thing is to set up two roslibjs topics, one pointing at the robot and one pointing at a complementary topic on the laptop.

this.recruitmentTopic = new ROSLIB.Topic({
   ros : this.robotRos,
   name: "/detectionLocations",
   messageType: "swarmie_msgs/Recruitment"
});
this.ltRecruitmentTopic = new ROSLIB.Topic({
   ros : this.laptopRos,
   name: "/detectionLocations",
   messageType: "swarmie_msgs/Recruitment"
});

We will listen on the robot topic relaying any messages that are tagged with the name this robot to the complementary laptop topic. Simultaneously we have subscribed to the laptop version of this topic. In this handler we do the complementary action relaying messages from the laptop to the robot only if the message is not tagged with the robot’s name. This selectivity is absolutely vital because it prevents an infinite feedback loop where the robot publishes a message that is relayed back to itself through the laptop at which point roslibjs sees the message and relays it again (and so on, and so on).

this.recruitmentTopic.subscribe(function(msg) {
   // only share our own recruitment messages
   if(msg.name.data == this.robotName)
   {
      this.ltRecruitmentTopic.publish(msg);
   }
});
this.ltRecruitmentTopic.subscribe(function(msg) {
   // only pass through messages that are from other robots
   if(msg.name.data != this.robotName)
   {
      this.recruitmentTopic.publish(msg);
   }
});
Published on 2019-07-09 Tue 10:59