Monitoreando obstaculos🔗
Esta es una traduccion a Rust de una implementacion hecha en Cpp por Justin Huang
aqui les dejo el video:
Me parecio interesante porque es como un "hola mundo" un poco mas avanzado. Ademas es una linda oportunidad para probar si podemos generar la misma ergonomia con Rust.
Definicion del problema🔗
Tenemos nuestro robot que publica su posicion a traves del topic /odom y
tenemos de antemano las posiciones de los obstaculos que estan en el mapa donde
transita el robot, entonces vamos a generar un nodo que monitorea cual es el
obstaculo mas cercano.
Solucion del problema en Cpp🔗
Para que podamos comparar las implementaciones, voy a poner aca la implementacion
en Cpp.
#include <vector>
#include <string>
#include "ros/ros.h"
#include "nav_msgs/Odometry.h"
#include "location_monitor/LandmarkDistance.h"
using location_monitor::LandmarkDistance;
struct Landmark {
std::string name;
double x;
double y;
};
struct LandmarkMonitor {
std::vector<Landmark> landmarks;
ros::Publisher landmark_publisher;
LandmarkMonitor(const ros::Publisher& landmark_pub): landmarks(), landmark_publisher(landmark_pub) {
init_landmarks();
}
double get_distance(Landmark landmark, double x, double y) {
double diff_x = landmark.x - x;
double diff_y = landmark.y - y;
return std::sqrt(diff_x * diff_x + diff_y * diff_y);
}
LandmarkDistance find_closest(double x, double y) {
LandmarkDistance result;
result.distance = -1;
for (size_t i = 0; i < landmarks.size(); ++i) {
const Landmark& landmark = landmarks[i];
double distance = get_distance(landmark, x, y);
if(result.distance < 0 || distance < result.distance) {
result.name = landmark.name;
result.distance = distance;
}
}
return result;
}
void OdomCallback(const nav_msgs::Odometry::ConstPtr& msg) {
double x = msg->pose.pose.position.x;
double y = msg->pose.pose.position.y;
LandmarkDistance landmarkd_distance = find_closest(x, y);
landmark_publisher.publish(landmarkd_distance);
}
void init_landmarks() {
landmarks.push_back(Landmark{"unit_box", 1.0, 1.0});
landmarks.push_back(Landmark{"unit_cylinder", 1.0, -1.0});
landmarks.push_back(Landmark{"unit_sphere", -1.0, 0.0});
}
};
int main(int argc, char** argv) {
ros::init(argc, argv, "location_monitor");
ros::NodeHandle node_handle;
ros::Publisher landmark_publisher = node_handle.advertise<LandmarkDistance>("closest_landmark", 10);
LandmarkMonitor monitor(landmark_publisher);
ros::Subscriber subscriber = node_handle.subscribe("odom", 10, &LandmarkMonitor::OdomCallback, &monitor);
ros::spin();
return 0;
}
Esta implementacion es mas "sencilla" que la hecha por Huang ya que usa solo structs
El robot que utilizo es el turtlebot3 y el mapa es uno hecho a partir del mapa empty
al que le agregue un cilindro, una esfera y un box. En esta implementacion el
utiliza un mensaje custom al que llamamos LandmarkDistance.msg que esta definido
como:
string name # este es al nombre que le asignamos a el landmark
float64 distance # esta es la distancia que nos separa del landmark
ROS genera el codigo de la definicion de esos types y los pone en un archivo .h
al que accedemos con:
#include "location_monitor/LandmarkDistance.h"
Hay que tener en cuenta que esto tiene que estar configurado en los archivos de
compilacion package.xml y CMakeLists.txt correctamente(si esto te parece chino
tenes que ver el tutorial oficial sobre mensajes)
Implementacion en Rust🔗
la implementacion en Rust es la siguiente:
use env_logger;
use rosrust;
use rosrust::Publisher;
use static_math::V2;
use std::sync::mpsc;
use std::time::Duration;
use rosrust_msg::location_monitor::LandmarkDistance;
#[derive(Debug)]
struct Landmark {
name: String,
position: V2<f64>
}
impl Landmark {
fn new(name: String, position: V2<f64>) -> Self {
Self{name, position}
}
}
struct LocationMonitor {
landmarks: Vec<Landmark>,
publisher: Publisher<LandmarkDistance>
}
impl LocationMonitor {
fn init(publisher: Publisher<LandmarkDistance>) -> Self {
let landmarks = vec![Landmark::new("unit_box".to_string(), V2::new_from(1.0, 1.0)),
Landmark::new("unit_cylinder".to_string(), V2::new_from(1.0, -1.0)),
Landmark::new("unit_sphere".to_string(), V2::new_from(-1.0, 0.0))];
Self{landmarks, publisher}
}
fn get_distance(landmark: &Landmark, point: V2<f64>) -> f64 {
let diff = landmark.position - point;
f64::sqrt(diff[0] * diff[0] + diff[1] * diff[1])
}
fn find_closest(&self, point: V2<f64>) -> LandmarkDistance {
let mut result = LandmarkDistance{name: "".to_string(), distance: -1.0};
for landmark in &self.landmarks {
let distance = Self::get_distance(landmark, point);
if result.distance < 0.0 || distance < result.distance {
result.name = landmark.name.clone();
result.distance = distance;
}
}
result
}
}
fn main() {
env_logger::init();
rosrust::init("location_monitor_rust");
let (tx, rx) = mpsc::channel();
let landmark_publisher = rosrust::publish("landmark_distance", 2).unwrap();
let monitor = LocationMonitor::init(landmark_publisher);
// NOTE(elsuizo:2021-07-21): nos subscribimos al topic "/odom" y pasamos ese mensaje a traves
// del thread
let subscriber_info = rosrust::subscribe("odom", 2, move |msg: rosrust_msg::nav_msgs::Odometry| {
// Callback for handling received messages
tx.send(msg).unwrap();
})
.unwrap();
while rosrust::is_ok() {
// NOTE(elsuizo:2021-07-21): esperamos la recepcion del mensaje
if let Ok(msg) = rx.recv_timeout(Duration::from_millis(10)) {
let x = msg.pose.pose.position.x;
let y = msg.pose.pose.position.y;
let position = V2::new_from(x, y);
let landmark_distance = monitor.find_closest(position);
monitor.publisher.send(landmark_distance);
}
}
rosrust::spin();
}
Como dijimos nos subscribimos al topic /odom con el que obtenemos la posicion
(x, y) del robot en el mapa con esta informacion calculamos la distancia con
respecto al conjunto de obstaculos y verificamos cual es el mas cercano. A esa
informacion la publicamos como un mensaje LandmarkDistance que definimos en
el codigo Cpp. Notemos que la implementacion de Rust genera tambien el codigo
necesario para esas definiciones sin que tengamos que hacer nada, solo la importamos
con la linea de codigo:
use rosrust_msg::location_monitor::LandmarkDistance;
Y como todo lo que definimos tiene su type bien establecido podemos incorporarlo al codigo sin problemas:
struct LocationMonitor {
landmarks: Vec<Landmark>,
publisher: Publisher<LandmarkDistance>
}
Una de las particularidades de la implementacion en Rust es que debemos utilizar
threads para compartir informacion y dado que el lenguaje nos garantiza que es
seguro no debemos preocuparnos por cosas que pasan en otros lenguajes como por
ejemplo data races
let subscriber_info = rosrust::subscribe("odom", 2, move |msg: rosrust_msg::nav_msgs::Odometry| {
// Callback for handling received messages
tx.send(msg).unwrap();
})
Aqui enviamos el msg a traves de un thread cada vez que recibimos el mismo en
el topic /odom. Como contrapartida hacemos la recepcion de manera segura con
if let Ok(msg) = rx.recv_timeout(Duration::from_millis(10)) {
let x = msg.pose.pose.position.x;
let y = msg.pose.pose.position.y;
let position = V2::new_from(x, y);
let landmark_distance = monitor.find_closest(position);
monitor.publisher.send(landmark_distance);
}
donde hacemos el llamado a los metodos que nos permiten calcular y publicar cual es el obstaculo que esta mas cerca.
Por ultimo podemos ver todo esto en accion en el siguiente video. Saludos!!!