Raspberry Pi based Autonomous UV Robot with SLAM

Published  February 25, 2022   0
R Robin Kanattu Thomas
Author
Autonomous UV Robot

The affordable autonomous and open-source robot provides localization and mapping facilities and safely navigates the robot through the environment. We are looking for open innovation so that designers, engineers, and hardware enthusiasts can come together to take this project to a new level and use this platform to build something different for humanity.

 

Component Required for Autonomous UV Robot with SLAM

UV Robot Hardware

Project Used Hardware

  • Raspberry Pi 3 Model B,
  • Arduino UNO,
  • Ydlidar X4,
  • UV lights,
  • Din 45 Ah battery,
  • Inverter 650 watts,
  • Seeed Wio Terminal,
  • ULTRAVIOLET UV DETECTION SENSOR,
  • 4-CHANNEL RELAY CONTROLLER FOR I2C
  • 12V to 5V buck converter
  • IB2 BTS7960 Monstor motor driver
  • DHT11 Temperature & Humidity Sensor (4 pins),
  • Arduino Mega 2560
  • Arduino Mega 2560 TFT Touchscreen,

Project Used Software

  • Arduino IDE,
  • ROS,
  • AutoDesk fusion 360,
  • MIT app inventor

Project Hardware Software Selection

As we face a worldwide healthcare crisis caused by COVID-19, there's a large need for disinfection. This tutorial can be followed by anyone who is new to ROS all the instructions are mentioned so that you too can build an autonomous UV bot. UV-C in many parts of the globe for fighting the COVID-19. Currently, such units are either stationary or moved by humans. We improved upon that concept and built an autonomous, mobile unit that may move around lobbies and hallways to get into rooms for disinfection. Our aim is to provide a simple tutorial to help people to make an affordable autonomous solution. DETAILS Three Lamps mounted on the robot would emit intense UV light at 240 nm wavelength, as per current medical device standards. Our key innovation is in making this mobile with a state-of-the-art LIDAR-based SLAM technology that allows operation without exposing the workers to either the harmful UV light or infected rooms of patients. The mobility allows the disinfection of various corners and narrow alleys. We will be using a mobile app to communicate with the robot and the user can see the real-time status of the robot. The bottom side can be used for other autonomous applications for surveillance, for delivery, etc.

Circuit Diagram

 

UV Robot Circuit Diagram

We are using the Wio terminal which has many inbuilt sensors. The Wio Terminal is a SAMD51-based microcontroller with Wireless Connectivity supported by Realtek RTL8720DN that’s compatible with Arduino and MicroPython. It runs at 120MHz (Boost up to 200MHz), 4MB External Flash, and 192KB RAM. It supports both Bluetooth and Wi-Fi providing the backbone for IoT projects. The Wio Terminal itself is equipped with a 2.4” LCD Screen, onboard IMU(LIS3DHTR), Microphone, Buzzer, microSD card slot, Light sensor, and Infrared Emitter(IR 940nm). Really cool features in one single device right. In our case, it acts as a wifi access point and we can connect the raspberry pi and our Virtual machine to a single access point. The inbuilt display will show the time and other messages. We are using the IMU sensor, inbuilt temperature sensor, and buzzer in the WIO terminal to detect a collision, overheating and it alerts through the buzzer as well as our mobile app. We are adding the UV light sensor to detect and get the feedback if UV light is ON or OFF. The WIO terminal is also connected to the relay to control the UV lights. The WIO terminal acts as an access point as well as a server. So that we can control the relay from UV SENSOR It uses a UV photodiode, which can detect the 240-370nm range of light. The signal from the photodiode is a very small level, in the nano-ampere level, hence an opamp to amplify the signal to a more Manageable voltage level. DHT sensor There is a thermistor. There is also a very basic chip inside that does some analog to digital conversion and shows a digital signal with the temperature and humidity. The digital signal is fairly easy to read using wio terminal. Here we are using it as a safety feature. The internal temperature of the robot is being measured here. If it is getting really high the complete system will shut down. UV light We are using 20w unit 2ft Sanyo Japan UVC lamp with a full assembly which provides an area coverage of 70 sqft which is sufficient for our application. You can refer to these articles to find out more regarding the disinfection properties of UVC light. PIR sensor PIR sensors allow you to sense motion, almost always used to detect whether a human has moved in or out of the sensor's range. They are small, inexpensive, low-power, easy to use, and don't wear out But it is a little inaccurate in our case we have to use Contact-less Infrared Thermopile Sensor with OpenCV to get more accurate data for human detection. We are working on the Thermopile sensor we will update in this tutorial really fast. MOTORS The initial plan was to use two NEMA 34 motors with 85KGCM Torque and use two DM860A2 motor drivers. But one of them was not working properly and so we changed our motors and drivers.So in this project, we will be using four wiper motors and two BTS7960 IBT2 motor divers instead. These motors can carry a sufficient load of our robot and control and coding is simple too. Charging circuit The robot identifies the charging port using path planning and comes and charges once every 5 hours. There are two rods on the robot and there are electrodes in the charging port which aligns properly. There is a magnetic reed switch that gets closed when the robot arrives.

Code
#include"LIS3DHTR.h"
LIS3DHTR<TwoWire> lis;

void setup() {
Serial1.begin(115200);
lis.begin(Wire1);
if (!lis) {
    Serial.println("ERROR");
    while(1);
  }
  lis.setOutputDataRate(LIS3DHTR_DATARATE_25HZ); //Data output rate
  lis.setFullScaleRange(LIS3DHTR_RANGE_2G); //Scale range set to 2g
}

void loop() {
 float x_values, y_values, z_values;
  x_values = lis.getAccelerationX();
  y_values = lis.getAccelerationY();
  z_values = lis.getAccelerationZ();
  Serial.print("X: "); Serial.print(x_values);
  Serial.print(" Y: "); Serial.print(y_values);
 Serial.print(" Z: "); Serial.print(z_values);
 Serial.print(" Sytem temp:"); Serial.println(lis.getTemperature());
}

Charging Circuit

#include <UTFT.h>

// Declare which fonts we will be using
extern uint8_t BigFont[];
UTFT myGLCD(ILI9481,38,39,40,41,42);// ElecFreaks TFT2.2SP Shield

// constants won't change. They're used here to set pin numbers:
const int Reedswitch = 11;     // the number of the pushbutton pin
const int ledPin =  10;
const int relay = 9; 
const int statusled = 12; 

// variables will change:
int buttonState = 0;         // variable for reading the pushbutton status

void setup() {
   randomSeed(analogRead(0)); 
// Setup the LCD
  myGLCD.InitLCD();
  myGLCD.setFont(BigFont);
  digitalWrite(statusled, HIGH);
  // initialize the LED pin as an output: 
  pinMode(ledPin, OUTPUT);
  pinMode(relay, OUTPUT);
  // initialize the pushbutton pin as an input:
  pinMode(Reedswitch, INPUT);
}

void loop() {
// Clear the screen and draw the frame
  //myGLCD.clrScr();

  // read the state of the pushbutton value:
  buttonState = digitalRead(Reedswitch);

  // check if the pushbutton is pressed. If it is, the buttonState is HIGH:
  if (buttonState == HIGH) {
    // turn LED on:
    digitalWrite(ledPin, HIGH);
    digitalWrite(relay, HIGH);
   myGLCD.clrScr();
  myGLCD.setColor(255, 255, 255);
  myGLCD.setBackColor(255, 0, 0);
  myGLCD.print("UVBOT is Charging!", CENTER, 93);
  } else {
    digitalWrite(ledPin, LOW);
    digitalWrite(relay, LOW);
     myGLCD.setColor(255, 255, 255);
  myGLCD.setBackColor(0, 0, 255);
  myGLCD.print("Charging circuit is ON!", CENTER, 93);
  delay(20);
  }
}

Pir test Wio Terminal

int led = D6;                // the pin that the LED is atteched to
int sensor = D4;              // the pin that the sensor is atteched to
int state = LOW;             // by default, no motion detected
int val = 0;                 // variable to store the sensor status (value)

void setup() {
  pinMode(led, OUTPUT);      // initalize LED as an output
  pinMode(sensor, INPUT);    // initialize sensor as an input
  Serial.begin(115200);        // initialize serial
}

void loop(){
  val = digitalRead(sensor);   // read sensor value
  if (val == HIGH) {           // check if the sensor is HIGH
    digitalWrite(led, HIGH);   // turn LED ON
    delay(100);                // delay 100 milliseconds  
    if (state == LOW) {
      Serial.println("Motion detected!"); 
      state = HIGH;       // update variable state to HIGH
    }
  } 
  else {
      digitalWrite(led, LOW); // turn LED OFF
      delay(200);             // delay 200 milliseconds  
      if (state == HIGH){
        Serial.println("Motion stopped!");
        state = LOW;       // update variable state to LOW
    }
  }
}

Ros Serial Motor Control 

#if (ARDUINO >= 100)
#include <Arduino.h>
#else
#include <WProgram.h>
#endif

#include <ros.h>
#include <geometry_msgs/Twist.h>
// Pin variables for motors.
const int right_back = 5;
const int right_front = 6;
const int left_back = 9;
const int left_front = 10;

ros::NodeHandle  nh;

void MoveFwd() {
  digitalWrite(right_back, HIGH);
  digitalWrite(left_back, HIGH);
  analogWrite(right_front,LOW);
  analogWrite(left_front, LOW);
}

void MoveStop() {
  digitalWrite(right_back, LOW);
  digitalWrite(left_back, LOW);
  analogWrite(right_front,LOW);
  analogWrite(left_front, LOW);
}
void MoveLeft() {
  digitalWrite(right_back, HIGH);
  digitalWrite(left_back, LOW);
  analogWrite(right_front,LOW);
  analogWrite(left_front, HIGH);
}
void MoveRight() {
  digitalWrite(right_back, LOW);
  digitalWrite(left_back, HIGH);
  analogWrite(right_front,HIGH);
  analogWrite(left_front, LOW);
}
void MoveBack() {
  digitalWrite(right_back, LOW);
  digitalWrite(left_back, LOW);
  analogWrite(right_front,HIGH);
  analogWrite(left_front, HIGH);
}

void cmd_vel_cb(const geometry_msgs::Twist & msg) {
  // Read the message. Act accordingly.
  // We only care about the linear x, and the rotational z.
  const float x = msg.linear.x;
  const float z_rotation = msg.angular.z;

  // Decide on the morot state we need, according to command.
  if (x > 0 && z_rotation == 0) {
    MoveFwd();
  }
  else if (x == 0 && z_rotation == 1) {
    MoveRight();
  }
else if (x == 0 && z_rotation < 0) {
    MoveLeft();
  }
else if (x < 0 && z_rotation == 0) {
    MoveBack();
  }
else{
    MoveStop();
  }
}
ros::Subscriber<geometry_msgs::Twist> sub("cmd_vel", cmd_vel_cb);
void setup() {
  pinMode(right_back, OUTPUT);    
  pinMode(left_back, OUTPUT);
  pinMode(right_front, OUTPUT);
  pinMode(left_front, OUTPUT); 
  nh.initNode();
  nh.subscribe(sub);
}

void loop() {
  nh.spinOnce();
  delay(1);
}

Sensor UV Test

//input this code to test UV sensor
void setup() {
    Serial.begin(115200);
    pinMode(A8, INPUT);
}
void loop() {
    int uvsensor = analogRead(A8);
    Serial.print("UV intensity: ");
    Serial.println(uvsensor);
    delay(50);
}

Serial Test

void setup() {
RTL8720D.begin(115200);
Serial.begin(115200);

}

void loop() {
 if (RTL8720D.available())
            {

                int inbyte = RTL8720D.read();

                Serial.write(inbyte)               

}
}

Wifi Realtek Development Board 

#include <WiFi.h>
// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0; 
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;
String header;

// Auxiliar variables to store the current output state
String output5State = "off";
String output4State = "off";
const int output5 =5;
const int output4 = 4;
char ssid[] = "UV bot";  //Set the AP's SSID
char pass[] = "12345678";     //Set the AP's password
char channel[] = "1";         //Set the AP's channel
int status = WL_IDLE_STATUS;  // the Wifi radio's status
WiFiServer server(80);
void printWifiStatus() {
    // print the SSID of the network you're attached to:
   // Serial.println();
   // Serial.print("SSID: ");
   // Serial.println(WiFi.SSID());

    // print your WiFi shield's IP address:
    IPAddress ip = WiFi.localIP();
    //Serial.print("IP Address: ");
   // Serial.println(ip);
}
void setup() {
    //Initialize serial and wait for port to open:
    Serial.begin(115200);
    Serial1.begin(115200);
    pinMode(output5,OUTPUT);
    pinMode(output4,OUTPUT);
    while (!Serial) {
        ; // wait for serial port to connect. Needed for native USB port only
    }

    // check for the presence of the shield:
    if (WiFi.status() == WL_NO_SHIELD) {
        //Serial.println("WiFi shield not present");
        while (true);
    }
    String fv = WiFi.firmwareVersion();
    if (fv != "1.0.0") {
       // Serial.println("Please upgrade the firmware");
    }

    // attempt to start AP:
    while (status != WL_CONNECTED) {
        //Serial.print("Attempting to start AP with SSID: ");
        //Serial.println(ssid);
        status = WiFi.apbegin(ssid, pass, channel);
        delay(10000);
    }

    //AP MODE already started:
   // Serial.println("AP mode already started");
    //Serial.println();
   server.begin();
   printWifiStatus();
}

void loop(){
  WiFiClient client = server.available();   // Listen for incoming clients

if (client) { // If a new client connects,
 Serial.println("New Client."); // print a message out in the serial port
 String currentLine = ""; // make a String to hold incoming data from the client
 while (client.connected()) { // loop while the client's connected
 if (client.available()) { // if there's bytes to read from the client,
  char c = client.read(); // read a byte, then
  Serial.write(c); // print it out the serial monitor
  header += c;
  if (c == '\n') { // if the byte is a newline character
   // if the current line is blank, you got two newline characters in a row.
   // that's the end of the client HTTP request, so send a response:
   if (currentLine.length() == 0) {
    // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
    // and a content-type so the client knows what's coming, then a blank line:
    client.println("HTTP/1.1 200 OK");
    client.println("Content-type:text/html");
    client.println("Connection: close");
    client.println();
    // turns the GPIOs on and off
if (header.indexOf("GET /5/on") >= 0) {
  //Serial.println("GPIO 5 on");
  output5State = "on";
  digitalWrite(output5, HIGH);
  //Serial.print("5");
} else if (header.indexOf("GET /5/off") >= 0) {
 // Serial.println("GPIO 5 off");
  output5State = "off";
  Serial.print("7");
  digitalWrite(output5, LOW);
} else if (header.indexOf("GET /4/on") >= 0) {
  //Serial.println("GPIO 4 on");
  output4State = "on";
  digitalWrite(output4, HIGH);
} else if (header.indexOf("GET /4/off") >= 0) {
  //Serial.println("GPIO 4 off");
  output4State = "off";
  digitalWrite(output4, LOW);
}

 client.println("<!DOCTYPE html><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            // CSS to style the on/off buttons 
            // Feel free to change the background-color and font-size attributes to fit your preferences
            client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
            client.println(".button { background-color: #195B6A; border: none; color: white; padding: 16px 40px;");
            client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
            client.println(".button2 {background-color: #77878A;}</style></head>");
client.println("<h1>UV BoT</h1>");
 client.println("<p>UV Light status " + output5State + "</p>");
            // If the output5State is off, it displays the ON button       
            if (output5State=="off") {
              client.println("<p><a href=\"/5/on\"><button class=\"button\">ON</button></a></p>");
            } 
            else {
              client.println("<p><a href=\"/5/off\"><button class=\"button button2\">OFF</button></a></p>");
            }
             client.println();
             break;
   }
     else { // if you got a newline, then clear currentLine
            currentLine = "";
          }
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    // Clear the header variable
    header = "";
    // Close the connection
    client.stop();
   // Serial.println("Client disconnected.");
   // Serial.println("");
  }
}

Wio Terminal Code

#include <SPI.h>

#define TFT_GREY 0x5AEB
#include"LIS3DHTR.h"
LIS3DHTR<TwoWire> lis;
#include"TFT_eSPI.h"
#include"Free_Fonts.h" //include the header file
TFT_eSPI tft;
uint32_t targetTime = 0;                    // for next 1 second timeout

static uint8_t conv2d(const char* p); // Forward declaration needed for IDE 1.6.x

uint8_t hh = conv2d(__TIME__), mm = conv2d(__TIME__ + 3), ss = conv2d(__TIME__ + 6); // Get H, M, S from compile time

byte omm = 99, oss = 99;
byte xcolon = 0, xsecs = 0;
unsigned int colour = 0;

void setup(void) {
  pinMode(WIO_BUZZER, OUTPUT);
    pinMode(A8, INPUT);
    pinMode(D4,OUTPUT);
  tft.begin();
  tft.setRotation(3);
  tft.fillScreen(TFT_BLACK);
   tft.setTextSize(1);
    tft.setTextColor(TFT_YELLOW, TFT_BLACK);

    targetTime = millis() + 1000;
  Serial1.begin(115200);
  RTL8720D.begin(115200);
  lis.begin(Wire1); 
  if (!lis) {
    Serial.println("ERROR");
    while(1);
  }
  lis.setOutputDataRate(LIS3DHTR_DATARATE_25HZ); //Data output rate
  lis.setFullScaleRange(LIS3DHTR_RANGE_2G); //Scale range set to 2g
}
void loop() {
  float x_values, y_values, z_values;
  x_values = lis.getAccelerationX();
  y_values = lis.getAccelerationY();
  z_values = lis.getAccelerationZ();
   int uvsensor = analogRead(A8)
 if (RTL8720D.available())
            {

                int inbyte = RTL8720D.read();

                if (inbyte>4) 
                digitalWrite(D4,HIGH);
                Serial.print("inbyte:"); Serial.write(inbyte);
                tft.drawString("   Caution! UV lamp is On      ",30,180 );
                }
                else if(inbyte<4)
                {
                  digitalWrite(D4,LOW);
              Serial.print("inbyte:");Serial.print(inbyte);
              tft.drawString("         UV lamp is Off                        ",30,180 ); 
              }
                else {
                }
                }
 // Serial.print("X: "); Serial.print(x_values);
 // Serial.print(" Y: "); Serial.print(y_values);
 // Serial.print(" Z: "); Serial.print(z_values);
  //Serial.println();
  if (x_values<0.5)
  {
     Serial.print("The Bot is down...stopping systems! ");
     Serial.print("temp:"); Serial.println(lis.getTemperature());
     RTL8720D.write("1");
     analogWrite(WIO_BUZZER, 128);
      tft.setFreeFont(FS12);
     tft.drawString("The Bot is down",70,140 );
     delay(50);
     Serial.print("uv value"); Serial.println(uvsensor);
     if (uvsensor> 400){
    Serial.print("UV Lamp is working: "); 
    tft.drawString("   Caution! UV lamp is On      ",30,180 );
   }
   else{
    tft.drawString("         UV lamp is Off                        ",30,180 ); 
   }   
  }
  else
  {Serial.print("temp:"); Serial.println(lis.getTemperature());
   tft.drawString("     UV Robot      ",70,140 ); 
   RTL8720D.write("0");
   analogWrite(WIO_BUZZER, 0);
   Serial.print("uv value"); Serial.println(uvsensor);
   if (uvsensor> 400){
    Serial.print("UV Lamp is working: ");
    tft.drawString("   Caution! UV lamp is On     ",30,180 );
   }
   else{
    tft.drawString("         UV lamp is Off                        ",30,180 ); 
   }
  }
  delay(50);
   if (targetTime < millis()) {
        // Set next update for 1 second later
        targetTime = millis() + 1000; 

        // Adjust the time values by adding 1 second
        ss++;              // Advance second
        if (ss == 60) {    // Check for roll-over
            ss = 0;          // Reset seconds to zero
            omm = mm;        // Save last minute time for display update
            mm++;            // Advance minute
            if (mm > 59) {   // Check for roll-over
                mm = 0;
                hh++;          // Advance hour
                if (hh > 23) { // Check for 24hr roll-over (could roll-over on 13)
                    hh = 0;      // 0 for 24 hour clock, set to 1 for 12 hour clock
                }
            }
        }
        // Update digital time
        int xpos = 0;
        int ypos = 20; // Top left corner ot clock text, about half way down
        int ysecs = ypos + 24;

        if (omm != mm) { // Redraw hours and minutes time every minute
            omm = mm;
            // Draw hours and minutes
            if (hh < 10) {
                xpos += tft.drawChar('0', xpos, ypos, 8);    // Add hours leading zero for 24 hr clock
            }
            xpos += tft.drawNumber(hh, xpos, ypos, 8);             // Draw hours
            xcolon = xpos; // Save colon coord for later to flash on/off later
            xpos += tft.drawChar(':', xpos, ypos - 8, 8);
            if (mm < 10) {
                xpos += tft.drawChar('0', xpos, ypos, 8);    // Add minutes leading zero
            }
            xpos += tft.drawNumber(mm, xpos, ypos, 8);             // Draw minutes
            xsecs = xpos; // Sae seconds 'x' position for later display updates
        }
        if (oss != ss) { // Redraw seconds time every second
            oss = ss;
            xpos = xsecs;

            if (ss % 2) { // Flash the colons on/off
                tft.setTextColor(0x39C4, TFT_BLACK);        // Set colour to grey to dim colon
                tft.drawChar(':', xcolon, ypos - 8, 8);     // Hour:minute colon
                xpos += tft.drawChar(':', xsecs, ysecs, 6); // Seconds colon
                tft.setTextColor(TFT_YELLOW, TFT_BLACK);    // Set colour back to yellow
            } else {
                tft.drawChar(':', xcolon, ypos - 8, 8);     // Hour:minute colon
                xpos += tft.drawChar(':', xsecs, ysecs, 6); // Seconds colon
            }

            //Draw seconds
            if (ss < 10) {
                xpos += tft.drawChar('0', xpos, ysecs, 6);    // Add leading zero
            }
            tft.drawNumber(ss, xpos, ysecs, 6);                     // Draw seconds
        }
    }
}
static uint8_t conv2d(const char* p) {
    uint8_t v = 0;
    if ('0' <= *p && *p <= '9') {
        v = *p - '0';
    }
    return 10 * v + *++p - '0';
}

ros Autonomous Setup:

import numpy as np
import math
import matplotlib.pyplot as plt
import rospy
from rospy.numpy_msg import numpy_msg
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist
from controller import PID
class WallFollower:
# Import ROS parameters from the "params.yaml" file.
# Access these variables in class functions with self:
# i.e. self.CONSTANT
SCAN_TOPIC = "/scan"
DRIVE_TOPIC = "cmd_vel"
SIDE = -1 # -1 right is and +1 is left
VELOCITY = 1.6
DESIRED_DISTANCE = 0.5
def __init__(self):
# Create a node that
#   Subscribes to the laser scan topic,
#   Publishes to  drive topic - to move the vehicle.
# Initialize subscriber to laser scan.
rospy.Subscriber(self.SCAN_TOPIC, LaserScan, self.LaserCb)
# Initialize a publisher of drive commands.
self.drive_pub = rospy.Publisher(self.DRIVE_TOPIC, Twist, queue_size = 10)
# Variables to keep track of drive commands being sent to robot.
self.seq_id = 0
# Class variables for following.
self.side_angle_window_fwd_ = math.pi*0.1
self.side_angle_window_bwd_ = math.pi - math.pi*0.3
self.point_buffer_x_ = np.array([])
self.point_buffer_y_ = np.array([])
self.num_readings_in_buffer_ = 0
self.num_readings_for_fit_ = 2
self.reject_dist = 0.7
self.steer_cmd = 0
self.vel_cmd = self.VELOCITY
self.pid = PID()
def GetLocalSideWallCoords(self, ranges, angle_min, angle_max, angle_step):
# Slice out the interesting samples from our scan. pi/2 radians from pi/4 to (pi - pi/4) radians for the right side.
positive_start_angle = self.side_angle_window_fwd_
positive_end_angle   = self.side_angle_window_bwd_
if self.SIDE == -1: #"right":
start_angle = -positive_start_angle
end_angle   = -positive_end_angle
elif self.SIDE == 1: #"left":
start_angle = positive_start_angle
end_angle   = positive_end_angle
start_ix_ = int((-angle_min +start_angle)/angle_step)
end_ix_ = int((-angle_min +end_angle)/angle_step)
start_ix = min(start_ix_,end_ix_)
end_ix = max(start_ix_,end_ix_)
side_ranges = ranges[min(start_ix,end_ix):max(start_ix, end_ix)]
x_values = np.array([ranges[i]*math.cos(angle_min+i*angle_step) if i < len(ranges) else  ranges[(i - len(ranges))]*math.cos(angle_min+(i - len(ranges))*angle_step) for i in range(start_ix, end_ix) ])
y_values = np.array([ranges[i]*math.sin(angle_min+i*angle_step) if i < len(ranges) else ranges[i - len(ranges)]*math.sin(angle_min+(i - len(ranges))*angle_step) for i in range(start_ix, end_ix)])
# Check that the values for the points are within 1 meter from each other. Discard any point that is not within one meter form the one before it.
out_x = []
out_y = []
for ix in range(0, len(x_values)):
new_point = (x_values[ix],y_values[ix])
# This conditional handles points with infinite value.
if  side_ranges[ix] < 2.5 and side_ranges[ix] > 0 and abs(new_point[1]) < 7000 and abs(new_point[0]) < 7000 :
out_x.append(new_point[0])
out_y.append(new_point[1])
return np.array(out_x), np.array(out_y)
def LaserCb(self, scan_data):
# This function is called every time we get a laser scan.
# This is the plan:
# * Get scan data.
# * Convert it to x,y coordinates in the local frame of the robot.
# * Find a least squares - best fit line through those points with numpy.
#   Consider using data from multiple scans in one least-squares fit cycle.
#   This is a line equation, with respect to the car at (0,0), with the x axis being the heading.
#   Get vector theta for the line, and theta_0 as the y intersection.
# * Find the distance from the line to the origin with ( theta_T dot [[0],[0]] + theta_0 ) / (norm theta)
# TLDR, We have a vector theta for the line we have found, and a distance to that wall.
# TODO(yorai): Handle erroneous scan values. If one is too big, or too small, use past value.
# Do not do this for too many in a row, maybe just throw scan away if too many corrections.
angle_step = scan_data.angle_increment
angle_min = scan_data.angle_min
angle_max = scan_data.angle_max
ranges = scan_data.ranges
# Get data for side ranges. Add to buffer.
wall_coords_local = self.GetLocalSideWallCoords(ranges, angle_min, angle_max, angle_step)
#######
#Find mean and throw out everything that is 1 meter away from mean distance, no outliers.
# If one differs by more than a meter from the previous one, gets thrown out from both x and y. Distnaces as we go along vector of points.
# but print the things first.
#######
self.point_buffer_x_ = np.append(self.point_buffer_x_, wall_coords_local[0])
self.point_buffer_y_ = np.append(self.point_buffer_y_, wall_coords_local[1])
self.num_readings_in_buffer_ +=1
# If we have enough data, then find line of best fit.
if self.num_readings_in_buffer_ >= self.num_readings_for_fit_:
# Find line of best fit.
# self.point_buffer_x_ = np.array([0, 1, 2, 3])
# self.point_buffer_y_ = np.array([-1, 0.2, 0.9, 2.1])
A = np.vstack([self.point_buffer_x_, np.ones(len(self.point_buffer_x_))]).T
m, c = np.linalg.lstsq(A, self.point_buffer_y_, rcond=0.001)[0]
# Find angle from heading to wall.
# Vector of wall. Call wall direction vector theta.
th = np.array([[m],[1]])
th /= np.linalg.norm(th)
# Scalar to define the (hyper) plane
th_0 = c
# Distance to wall is (th.T dot x_0 + th_0)/(norm(th))
dist_to_wall = abs(c/np.linalg.norm(th))
# Angle between heading and wall.
angle_to_wall = math.atan2(m, 1)
# Clear scan buffers.
self.point_buffer_x_=np.array([])
self.point_buffer_y_=np.array([])
self.num_readings_in_buffer_ = 0
# Simple Proportional controller.
# Feeding the current angle ERROR(with target 0), and the distance ERROR to wall. Desired error to be 0.
print("ANGLE", angle_to_wall, "DIST", dist_to_wall)
steer = self.pid.GetControl(0.0 - angle_to_wall, self.DESIRED_DISTANCE - dist_to_wall, self.SIDE)
# Publish control to /drive topic.
drive_msg = Twist()
# drive_msg.header.seq = self.seq_id
# self.seq_id += 1
# Populate the command itself.
drive_msg.linear.x = self.VELOCITY
drive_msg.angular.z = steer
# drive_msg.drive.steering_angle = steer
# drive_msg.drive.steering_angle_velocity = 0.1
# drive_msg.drive.speed = self.VELOCITY
# drive_msg.drive.acceleration = 1
# drive_msg.drive.acceleration = 0.5
self.drive_pub.publish(drive_msg)
if __name__ == "__main__":
rospy.init_node('wall_follower')
wall_follower = WallFollower()
rospy.spin()


Video