The DIY Remote Controlled Robot with an Autonomous Mode

This is my first attempt to build a remote controlled robot with an autonomous mode.

I was thinking about some Roomba-style autonomous Arduino robot for some time now. It could be used to monitor the home environment and keep my cat busy when we’re away.

Teodor The Bored Cat

This project is my first attempt in the world of Arduino robotics and the first step towards something more useful. Subscribe to the newsletter if you want to be notified about next tutorials where I’ll demonstrate upgrades and further development. It’s my first C++ project so please be lenient if you see any anti-patterns. Of course feel free to comment if you feel something could be improved.

Requirements

First, let’s focus on the requirements.

Occasionally autonomous arduino robot should be able:

  • explore area
  • move forward/backward and turn left/right during the explorations
  • avoid collisions with the obstacles
  • have an easy way to expand the case
  • use 9V battery as the power source

Remotely controlled  arduino robot’s operator should be able:

  • to use a standard TV remote
  • to turn left/right, move forward/backward with help of the remote
  • to switch autonomous mode on and off

Arduino Robot

Next, let’s see which components we will use to make robot move and feel.

Arduino Robot’s frame

For the robot’s case, I found this cool modular robot frame project and 3D printed it. I had continuous servos at home so it was a great fit. Unfortunately, this frame happened to have some drawback, like not being able to climb on carpets. The next project, I’ll choose a more versatile chassis.

All robot’s files and instructions are available here:

http://www.thingiverse.com/thing:1720394

Distance sensor

I had decided to use simple ultrasonic proximity sensor HC-SR04.

An ultrasonic sensor consists of one or more ultrasonic transmitter/s (basically speakers), a receiver, and a control circuit. The transmitters emit a high-frequency ultrasonic sound, which bounces off any nearby objects. Some of that ultrasonic sound is reflected and identified by the receiver on the sensor. That return signal is processed by the control circuit to calculate the time difference between the transmitted signal and received one. Finally, this time can be used, along with some calculations, to determine the distance between the sensor and the reflecting object.

Motors

The choice of the frame determined my choice of motors. I’ve used continuous rotation servo motors.

This servo rotates fully forward or backward instead of moving to a position. You can use any servo code, hardware or library to control these servos. Good for making simple moving robots.

An IR sensor and a remote

I bought the simple “KEYES” KY-022 IR receiver module which uses an 1838 infrared receiver.

It’s receiving angle is 90°, the distance 18 m, the operating voltage 2.7-5.5V and the operating frequency 37.9KHZ.

As a remote controller, I’ve used some random IR TV Remote. Every key on the remote sends specific code, identifying which key has been pressed. I’ve decided to use up/down, left/right buttons to control Arduino robot’s movement directions and Enter/OK button to toggle autonomous mode on and off.

Assembly

3D printing and robot assembly instruction please download here:
http://www.thingiverse.com/thing:1720394/#files

The diagram below shows how to connect sensors and motors.

The assembled robot should look like that.

Programming

The code base for an arduino robot can grow with time. Arduino robot projects tend to has a complex and tangled control structure. Our code needs to be modular, easy to modify, scale and manage.  We can achieve it using classes, inheritance and all OOP (Object Oriented Programming) goodies. We create separate drivers for every major hardware component we use. Code comments should make everything clear. The Arduino Robot’s logic should be separate from components implementation.

Note: I have extensive knowledge about high-level OOP (Java, C#), but as I mentioned before I’m not an expert in C++/C, so I’m not sure if my code is optimal. In case You feel something should be done differently just give me a hint in the comments section.

I’ve built a small library where I’ll keep all the drivers for the components used in my projects. It’s available on github.
https://github.com/robertmeisner/useful-drivers

Most up to date version of the robot’s source code is available in a GitHub repo. Just clone it and have fun.

https://github.com/robertmeisner/cat-robot

to clone this repository use –recursive flag as it uses sub-modules feature to link other repos.

git clone --recursive https://github.com/robertmeisner/cat-robot.git

You can watch a fast-forward video describing how to clone and upload code to Arduino.

Motor

To understand how The Motor Driver works see Arduino Continuous Rotation (360 degree) Servo Driver Implementation.

https://github.com/robertmeisner/useful-drivers/tree/master/motor

Distance sensor

To understand how The Distance Sensor Driver works see Arduino HC-SR04 Distance Sensor Driver Implementation.

https://github.com/robertmeisner/useful-drivers/tree/master/distance

Infrared

To understand how The Distance Sensor Driver works see Arduino Infrared (IR) Remote Driver Implementation.

https://github.com/robertmeisner/useful-drivers/tree/master/IR

The Autonomous Arduino Robot

Here sits all the Arduino robot’s logic.

/**
 * Cat Robot Driver Implementation with servos, distance sensor, IR remote and autonomous mode!
 *
 * @file motor_driver.h
 * @brief Robot Driver Implementation
 * @author Robert Meisner <robert@catchit.pl>
 * @version 1.0 2016-12-22
 */
#include "../motor/continous_servo_motor_driver.h"
#include "../distance/hc_sr04_distance_sensor_driver.h"
#include "../IR/keyes_ir_remote_driver.h"
// Pin assignment and servo declarations

namespace CatchIT
{

enum State
{
    TURNING_LEFT,
    TURNING_RIGHT,
    MOVING_FORWARD,
    MOVING_BACKWARD,
    STOPPED
};
class Robot
{
  public:
    /*
         * @brief Class constructor.
         */
    Robot(int trigPin,
          int echoPin,
          int leftMotorPin,
          int rightMotorPin)
        : trigPin(trigPin), echoPin(echoPin), leftMotorPin(leftMotorPin), rightMotorPin(rightMotorPin)
    {
        //create distance sensor instance
        distanceSensor = new CatchIT::HC04DistanceSensorDriver(trigPin, echoPin, (long)maxDistance);
        //create left motor instance
        leftMotor = new CatchIT::ContinousServoMotorDriver(leftMotorPin);
        //create right motor instance
        rightMotor = new CatchIT::ContinousServoMotorDriver(rightMotorPin);
        //create remote instance
        remote = new CatchIT::KeyesIRRemoteDriver(12);
    }
    /**
    * Inits robot's components
     */
    void init()
    {
        // init drivers
        distanceSensor->init();
        leftMotor->init();
        rightMotor->init();
        remote->init();
        // if analog input pin 0 is unconnected, random analog
        // noise will cause the call to randomSeed() to generate
        // different seed numbers each time the sketch runs.
        // randomSeed() will then shuffle the random function.
        randomSeed(analogRead(0));
        move(0, 0);
    }
    /**
    * Explore function executes logic
    *
    * @return True if robot can continue the exploration, iforms that explore() can be called again, false when robot should finish its operation or a critical error has occured. No subsequent call to explore() should be performed.
    */
    bool explore()
    {

        if (operationTimeLeft > millis())
        {
            Serial.print("Time to finish: ");
            Serial.println(operationTimeLeft - millis());
        }
        else
        {
            //Serial.print("Time to finish: ");
            //Serial.println("Negative number");
        }
        //get distance
        unsigned long distance = distanceSensor->getDistance();
        bool obstacle = distance < tooClose;
        //IS MANUAL MODE SWITCH BUTTON PRESSED?
        bool keyPressed = remote->keyPressed();
        if (keyPressed && remote->isOK())
        {
            MANUAL_CONTROL = !MANUAL_CONTROL;
            move(0, 0);
            return true;
        }
        // PERFORM DIFFERENT BEHAVIOUR FOR MANUAL AND AUTONOMOUS MODE
        switch (MANUAL_CONTROL)
        {
        case true:

            if (((isMoving() && moveFinished()) || (isTurning() && turnFinished())))
            {
                //stop
                move(0, 0);
            }
            else
            {
                if (keyPressed)
                { //we have received an IR

                    if (remote->isUp())
                    {
                        move(255, 1000);
                    }
                    else if (remote->isDown())
                    {
                        move(-255, 1000);
                    }
                    else if (remote->isLeft())
                    {
                        turn(-30);
                    }
                    else if (remote->isRight())
                    {
                        turn(30);
                    }

                    Serial.println(remote->value(), HEX); //display HEX
                                                          //next value
                }
                else
                {
                    // do nothing
                }
            }

            break;
        case false:

            // We always check distance first
            if ((isMoving() && state != MOVING_BACKWARD) && obstacle)
            {
                //stop
                move(0, 0);
                // move back a bit
                move(-200, 1000 * random(1, 2));

            } //if just moved back rotate him
            else if (isMoving() && moveFinished() && state == MOVING_BACKWARD)
            {
                //turn semi randomly
                turn((random(1) ? -1 : 1) * 20 + random(10));

            } // if he just turned lets move forward
            else if (isTurning() && turnFinished())
            {
                move(255, 1000 * random(4, 10));
            }
            //if he just moved forward
            else if (isMoving() && moveFinished() && state != MOVING_BACKWARD)
            {
                turn((random(1) ? -1 : 1) * 20 + random(10));
            }
            //if we stopped for some reason lets turn
            else if (isStopped())
            {
                turn((random(1) ? -1 : 1) * 20 + random(10));
            }
            break;
        }
        return true;
    }
    /**
    * Checks if robot turns
    */
    bool isTurning()
    {
        return (state == TURNING_LEFT || state == TURNING_RIGHT);
    }
    /**
    * Checks if robots last action was turn and it has finished
    */
    bool turnFinished()
    {
        if (isTurning() && operationTimeLeft > millis())
        {
            return false;
        }
        return true;
    }
    /**
    * Checks if robot moves
    */
    bool isMoving()
    {
        return (state == MOVING_FORWARD || state == MOVING_BACKWARD);
    }
    /**
    * Checks if robots last action was move and it has finished
    */
    bool moveFinished()
    {
        if (isMoving() && operationTimeLeft > millis())
        {
            return false;
        }
        return true;
    }
    /**
    * Checks if robot stopped
    */
    bool isStopped()
    {
        return state == STOPPED;
    }
    /**
    * Turns robot left (angle<0) or right (angle>0) for a given angle.
    * @todo executing turn should use some sensor data - not only the timer
    * @param angle Angle robot should turn. For angle<0 turns left, for angle>0 turns right
    */
    void turn(int angle)
    {

        int timeToTurn90 = 500;
        int timetoTurn = timeToTurn90 * ((float)angle / (float)90);
        Serial.print("Turning angle: ");
        Serial.println(angle);
        Serial.print("Turning timetoTurn: ");
        Serial.println(timetoTurn);
        setOperationTime(timetoTurn);
        if (angle >= 0)
        {
            state = TURNING_RIGHT;
            leftMotor->setSpeed(-255);
            rightMotor->setSpeed(-255);
        }
        else
        {
            state = TURNING_LEFT;
            leftMotor->setSpeed(255);
            rightMotor->setSpeed(255);
        }

        //turn for
        //set state to TURNING
        //adjust turning time
    }
    /**
    * Moves robot
    * @param speed Speed robot should move
    * Valid values are between -255 and 255. 
    * Use positive values to run the motor forward, 
    * negative values to run it backward and zero to stop the motor.
    * @param timeToMove Time limit robot should move in that direction
    */
    void move(int speed, int timeToMove)
    {

        if (speed == 0)
        {
            state = STOPPED;
            Serial.print("Moving stopped: ");
        }
        else if (speed > 0)
        {
            state = MOVING_FORWARD;
            Serial.print("Moving forward: ");
        }
        else
        {
            state = MOVING_BACKWARD;
            Serial.print("Moving backward: ");
        }
        int leftSpeed = speed * -1;
        int rightSpeed = speed;

        setOperationTime(timeToMove);
        leftMotor->setSpeed(leftSpeed);
        rightMotor->setSpeed(rightSpeed);
        Serial.print(leftSpeed);
        Serial.print(" - ");
        Serial.print(rightSpeed);
        //Serial.println(speed);
        Serial.print("Moving timeToMove: ");
        Serial.println(timeToMove);
    }
    setOperationTime(long time)
    {
        operationTimeLeft = millis() + time;
    }

  private:
    //Arduino specific - probably should move it somewhere else
    int trigPin = 2;
    int echoPin = 4;
    int leftMotorPin = 9;
    int rightMotorPin = 8;

    State state;
    // MANUAL SWITCH ON/OFF
    bool MANUAL_CONTROL = true;
    // every operation is timed. this is timer for current operation
    unsigned long operationTimeLeft = 0;
    // the distance robot should keep from obstacles
    int tooClose = 200;
    // components declarations
    int maxDistance = tooClose * 10;
    CatchIT::HC04DistanceSensorDriver *distanceSensor = NULL;
    CatchIT::ContinousServoMotorDriver *leftMotor = NULL;
    CatchIT::ContinousServoMotorDriver *rightMotor = NULL;
    CatchIT::KeyesIRRemoteDriver *remote = NULL;
};
}

 

Arduino Loop

Finally just create robot’s instance, init it and run in the loop.

/**
 * @file cat_robot_distance.ino
 * @brief Arduino remotely controlled robot with autonomous mode 
 * @author Robert Meisner <robert@catchit.pl>
 */

#include "drivers/robot/robot_driver.h"
int trigPin = 2;
int echoPin = 4;
int leftMotorPin = 9;
int rightMotorPin = 8;

CatchIT::Robot robot(trigPin,
                     echoPin,
                     leftMotorPin,
                     rightMotorPin);
/**
 * Init the robot
 */
void setup() 
{
  Serial.begin(9600);
  robot.init();
}
/**
 * Program's loop function calling robot.explore().
 * Every call of robot.explore() function runs  robots behavior routines.
 */
void loop()
{

  while(robot.explore()){
    
  }
}

 

Final result