Feroz FernandoAndre S. Manurung
Published © GPL3+

Low Budget Carriage Robot

A low-budget and sufficiently capable to replace the existing, expensive indoor carriage robot, based on line following and decoding system.

AdvancedFull instructions providedOver 2 days2,654
Low Budget Carriage Robot

Things used in this project

Hardware components

Espressif ESP32
×1
Stepper motor driver board A4988
SparkFun Stepper motor driver board A4988
×2
SparkFun Logic Level Converter - Bi-Directional
SparkFun Logic Level Converter - Bi-Directional
×2
5-Channel Line Following Sensor
×1
DFRobot DFPlayer
×1
LM2596 Buck Converter
×1
Jumper wires (generic)
Jumper wires (generic)
×30
Resistor 100k ohm
Resistor 100k ohm
×4
Capacitor 100 µF
Capacitor 100 µF
×2
Big Red Dome Button
SparkFun Big Red Dome Button
×1
Speaker: 3W, 4 ohms
Speaker: 3W, 4 ohms
×1
LM386 Audio Amplifier Module
×1
Storage shelf
×1
3S Li-Po Battery 2200 mAh
Any connector will do fine
×1
3S Li-Poly Battery Charger
Preferably plug and charge chargers
×1
Nema-17 Stepper Motor
×2
Motor Mounting Bracket
×1
5mm to 5mm Aluminum Shaft Coupler
×1
20mm M3 PCB Spacer
×1
Machine Screw, M3
Machine Screw, M3
10-15 mm long will do fine
×10
Machine Screw, M4
Machine Screw, M4
10-15 mm long will do fine
×10
M5 screw
20-25 mm long will do fine
×10
Connector Accessory, Hex Nut
Connector Accessory, Hex Nut
For the screws and spacers, M3, M4, and M5 size
×50
10 cm x 10 cm x 5 mm Acrylic Sheet
Round shape can work as well
×1
Single Side Silicon Suction Pad
×1

Software apps and online services

Arduino IDE
Arduino IDE
Android Studio
Android Studio
Firebase
Google Firebase

Hand tools and fabrication machines

Drill / Driver, Cordless
Drill / Driver, Cordless
Multitool, Screwdriver
Multitool, Screwdriver
Soldering iron (generic)
Soldering iron (generic)
Solder Flux, Soldering
Solder Flux, Soldering
Plier, Long Nose
Plier, Long Nose
For screws and bolt tightening
Precision screwdriver set
3-cm Wide Duct/Electrical Tape

Story

Read more

Custom parts and enclosures

3D Sketches

Contains wheel setup and base hole arrangement

Audio files

Audio for DFPlayer

Schematics

Fritzing Schematic

Code

Main ESP Code

Arduino
It contains 2 files, both files need to be in the same folder
#include <string.h>
#include <stdlib.h>
#include <BluetoothSerial.h>
#include <FirebaseESP32.h>
#include <EEPROM.h>
#include <WiFi.h>
#include <AccelStepper.h>
#include <DFRobotDFPlayerMini.h>

//Preprocessors
#define EEPROM_SIZE 98

//Firebase credentials
#define FIREBASE_HOST "hackster-2b8b9.firebaseio.com"
#define FIREBASE_AUTH "0G01CqZ0QGV10c2ajqO3REGTx9IKHFLoXP06Kyk2"

//Pins
#define leftStep 18
#define leftDir 5
#define rightStep 19
#define rightDir 21
#define motorInterfaceType 1

#define left2pin 14
#define left1pin 27
#define midpin 32
#define right1pin 34
#define right2pin 39

#define buttonPin 13
#define battPin 15
#define ledPin 2

//Motor speed
#define spd 110

//Objects
AccelStepper leftMotor(motorInterfaceType, leftStep, leftDir);
AccelStepper rightMotor(motorInterfaceType, rightStep, rightDir);
BluetoothSerial SerialBT;
DFRobotDFPlayerMini myDFPlayer;

//Variables
int state = 0,
    lastState,
    lastJunction,
    totalRooms, 
    totalTargetRooms,
    count = 0,
    roomCount = -1,
    targetRoomCount = 0;

String ssid = "",
       password = "",
       robotID = "LBR01";
       
FirebaseData rooms,
             roomCommands,
             roomNow,
             notify,
             battery;

char *currentRoom,*currentTarget,
     *roomList[100], *targetRooms[100];

bool left2,left1,
     mid,
     right1,right2,
     allDone = 0,
     wifiState = 0,
     roomChecking = 0,
     notification = 0,
     fromRoom = 0;

unsigned long prevTime = 0;

void setup() {
  //Pin initializations
  pinMode(ledPin,OUTPUT);
  pinMode (buttonPin, INPUT_PULLUP);
  pinMode (battPin, INPUT);
  pinMode (left2pin, INPUT);
  pinMode (left1pin, INPUT);
  pinMode (midpin, INPUT);
  pinMode (right1pin, INPUT);
  pinMode (right2pin, INPUT);
  
  EEPROM.begin(EEPROM_SIZE); //EEPROM to store SSID and Password
  Serial.begin(115200); //Serial monitor for debugging
  Serial2.begin(9600); //Serial 2 pins are GPIO16 and GPIO17 for RX and TX respectively
  myDFPlayer.begin(Serial2); //Initialize DFPlayer
  myDFPlayer.volume(20);
  
  delay(100);
  if (!myDFPlayer.begin(Serial2)) {
    Serial.println(("Unable to begin:"));
    Serial.println(("1.Please recheck the connection!"));
    Serial.println(("2.Please insert the SD card!"));
  }
  Serial.println(F("DFPlayer Mini online."));
  delay(1000);

  //Set motor parameters, 1000 is top speed
  leftMotor.setMaxSpeed(1000);
  leftMotor.setAcceleration(10);
  rightMotor.setMaxSpeed(1000);
  rightMotor.setAcceleration(10);

  //Check whether EEPROM contains SSID and Password or not
  if (EEPROM.read(0) != EEPROM_SIZE-1){
    ssid = readStringFromEEPROM(0);
    password = readStringFromEEPROM(32);
  }

  //Try connect to WiFi, if failed, get SSID and Password via
  //Bluetooth. If connection succeed, connect to firebase database
  if (ssid != "" && password != "") wifiState = connectWifi();
  if (wifiState != 0){
    Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);
    delay(10);
    state = 1;
  }
  else{
    state = 0;
  }

  //Read sensors for initialization so the values wont be undefined
  readSensors();
  delay(2000);
}

void loop() {
  //Pre-Initialization State, get SSID and Password via Bluetooth
  if (state == 0){
    digitalWrite(ledPin,LOW);
    ssid = "";
    password = "";
    getSSIDPassword();
    if (ssid != "" && password != ""){
      state = 1;
      //End bluetooth connection if we already got the SSID and Password
      SerialBT.end();
    }
    else{
      state = 0;
    }
    lastState = 0;
  }
  //Initialization State, get the neccessary data from Firebase server
  else if (state == 1){
    /*Connect to Wifi, if connection successful, connect to firebase
     *get the available rooms data and rooms to visit data,
     *then turn the string into an array of string
     */
    if (lastState == 0){
      wifiState = connectWifi();
      Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);
    }
    targetRoomCount = 0;
    roomCount = -1;
    if (wifiState){
      digitalWrite(ledPin,HIGH);
      allDone = 0;
      //Get battery status, the low voltage (11,1 V) translates to 3607 after it got into voltage divider
      //Map the processed value from 0 to 100
      int batt = map(analogRead(battPin), 3607, 4095, 0, 100);

      //Get data from firebase
      Firebase.getString(rooms, "ID_Robot/" + robotID + "/listRoom");
      Firebase.getString(roomCommands, "ID_Robot/" + robotID + "/commandRoom");
      Firebase.getBool(notify, "ID_Robot/" + robotID + "/notifyRobot");
      Firebase.setString(roomNow, "ID_Robot/" + robotID + "/locNow", "start");
      Firebase.setInt(battery, "ID_Robot/" + robotID + "/battRobot", batt);
      notification = notify.boolData();

      //Blink LED as a notification to user
      if (notification == 1){
        for (int i = 0; i<4; i++){
          digitalWrite(2,LOW);
          delay(300);
          digitalWrite(2,HIGH);
          delay(300);
        }
        Firebase.setBool(notify, "ID_Robot/" + robotID + "/notifyRobot", 0);
      }

      //Convert received room data to array of string
      String allRooms = rooms.stringData();
      String _targetRooms = roomCommands.stringData();
      stringToArray(allRooms, ",", &totalRooms, roomList);
      stringToArray(_targetRooms, ",", &totalTargetRooms, targetRooms);
      
      //If there is already a target room
      if (totalTargetRooms > 1){
        currentTarget = targetRooms[0];
        currentRoom = "start";
        state = 2;
      }
      //No target rooms, wait for target rooms
      else{
        state = 1;
      }
    }
    //Connection to WiFi failed, try to get SSID and Password again
    else{
      Serial.println("Failed to Connect to Wifi");
      ssid = "";
      password = "";
      state = 0;
    }
    lastState = 1;
  }
  
  //Moving state, only when initialization is done
  else if (state != 1 && state != 0){
    //Read the line following sensors first, then decide 
    //what state it should be in
    readSensors();
    //printSensors();
    //The sensors will be OFF when it hit the line
    //This state is a forward state
    if (left2 && left1 && right1 && right2){
      state = 2;
    }
    //The robot is swayed to the right, so go to the left
    else if (left2 && left1 && !right1 && right2){
      state = 3;
    }
    //The robot is swayed to the left, so go to the right
    else if (left2 && !left1 && right1 && right2){
      state = 4;
    }
    //A horizontal line to the left is detected, the below statement
    //will decide what to do
    else if (!left2 && right2){
      state = 5;
    }
    //A horizontal line to the right is detected, the below statement
    //will decide what to do
    else if (left2 && !right2){
      state = 6;
    }
    //There is 32 combinations of sensor states available, but not all is needed.
    
    //Prioritize this reading, so it is separated from previous "if"s
    
    if (left2 && left1 && mid && right1 && right2){ //No line is detected
      state = 7;
    }
    if (!left2 && !left1 && !mid && !right1 && !right2){ //A full horizontal line is detected
      state = 8;
    }
    
Serial.print("State : ");
Serial.println(state);
    //Movements
    if (state == 2){
      goForward();
      lastState = 2;
    }
    else if (state == 3){
      goRight();
      lastState = 3;
    }
    else if (state == 4){
      goLeft();
      lastState = 4;
    }
    else if (state == 5){
      //If a line to the left is detected, then check is it a room or not.
      //A room is marked with a single dashed line
      //If it is a room,update the robot location, then check whether it is the target room or not.
      //If it isnt, then keep going forward, if it is, then pivot to the left
      //If it is NOT a room, then it is a regular junction, and pivot to the left
      if (roomChecking){
        //If current room = target room, then turn, using strcmp from stdlib
        //Else go forward, block all readings
        if (strcmp(currentRoom,currentTarget) == 0){
          lastJunction = 5;
          pivotLeft();
        }
        else{
          goForward(100);
        }
        roomChecking = 0;
      }
      else{
        pivotLeft();
      }
      lastState = 5;
    }
    else if (state == 6){
      //If a line to the right is detected, then check is it a room or not.
      //A room is marked with a single dashed line
      //If it is a room, check whether it is the target room or not
      //If it isnt, then keep going forward, if it is, then pivot to the right
      //If it is NOT a room, then it is a regular junction, and pivot to the right
      if (roomChecking){
        //If current room = target room, then turn
        //Else go forward, block all readings
        if (strcmp(currentRoom,currentTarget) == 0){
          lastJunction = 6;
          pivotRight();
        }
        else{
          goForward(100);
        }
        roomChecking = 0;
      }
      else{
        pivotRight();
      }
      
      lastState = 6;
    }
    else if (state == 7){
      /* There is two conditions if there is no line is detected:
       * 1. It might be a dashed line, meaning there is a room onward
       * 2. It might be a sign to stop because there is no more line, either the robot moves off track (less likely)
       * or it has arrived in front of a door
       * 3. The robot came from a room and going to continue its progress
       * To check these conditions, move forward for around 5-7 cms (estimated 32 stepper motor steps)
       */
      //Dashed line condition
      if (!fromRoom){
        if (lastState != 7){
          prevTime = millis();
          lastState = 7;
        }
        //Go forward for 500 ms while checking if there is any change in sensor readings
        while (millis()-prevTime < 500){
          roomChecking = 0;
          goForward();
          readSensors();
          if (!left2 || !left1 || !mid || !right1 || !right2){
            roomChecking = 1;
            if(roomCount < totalRooms){
              if (!allDone) roomCount++;
              if (allDone) roomCount--;
              //Update the current location of the robot
              currentRoom = roomList[roomCount];
              Firebase.setString(roomCommands, "ID_Robot/" + robotID + "/locNow", currentRoom);
            }
            else{ //All room is already checked
              allDone = 1;
            }
            break;
          }
        }
      }
      
      else{
        //Third condition, if the robot is coming from a room to continue progress
        //If the room is on the left, then pivot to the left to continue, or pivot to the right to go back
        //Vice versa
        fromRoom = 0;
        if (!allDone){
          if (lastJunction == 5){
            pivotLeft(270);
            goForward(50);
          }
          else if (lastJunction == 6){
            pivotRight(270);
            goForward(50);
          }
        }
        else{
          if (lastJunction == 5){
            pivotRight(270);
            goForward(50);
          }
          else if (lastJunction == 6){
            pivotLeft(270);
            goForward(50);
          }
        }
      }
      //Second condition, just stop until any given input
      brake();
      //Play sound if it is in front of a door
      if (strcmp(currentRoom, currentTarget) == 0 && !allDone && roomChecking == 0){
        playRoomSound(roomCount+1);
      }
      //Wait for user's input, indicating the user have received the goods
      int button = digitalRead(buttonPin);
      if (button == 0){
        fromRoom = 1;
        if (targetRoomCount < totalTargetRooms-2){
          targetRoomCount++;
        }
        else{
          allDone = 1;
        }
        currentTarget = strdup(targetRooms[targetRoomCount]);
        delay(1000);
        //If button pressed, turn around, then go forward for a bit to get in track/line
        turnAround();
        goForward(100);
      }
    }
    //A full line is detected
    else if (state == 8){
      brake();
      //If all the room commands already visited, back to original position
      if (allDone){
        turnAround();
        allDone = 0;
        Firebase.setString(roomCommands, "ID_Robot/" + robotID + "/commandRoom", "0");
        Firebase.setString(roomCommands, "ID_Robot/" + robotID + "/locNow", "start");
        state = 1;
      }
    }
  }
}

Functions file

Arduino
A part of the main code file, both need to be in the same folder before compiling
void readSensors(){
  left2 = digitalRead(left2pin);
  left1 = digitalRead(left1pin);
  mid = digitalRead(midpin);
  right1 = digitalRead(right1pin);
  right2 = digitalRead(right2pin);
}

int connectWifi(){
  prevTime = millis();
  //Convert the string to array of char to make it compatible with the WiFi.begin
  //function
  WiFi.begin(ssid.c_str(), password.c_str());
  while (WiFi.status() != WL_CONNECTED) {
    //Wifi connection timeout status
    if (millis()-prevTime > 3000){
      ssid = "";
      password = "";
      return 0;
    }
  }
  return 1;
}

void getSSIDPassword(){
  //Start bluetooth connection
  if (!SerialBT.isReady()){
    SerialBT.begin(robotID);
    delay(200);
  }
  int getSSIDPass = 0;
  //Wait for bluetooth estabilishment
  //Get incoming data
  while (SerialBT.available()){
    char incomingChar = SerialBT.read();
    if (incomingChar == '\n'){
      getSSIDPass = 1;
    }

    if (getSSIDPass == 0){
      ssid += String(incomingChar);
    }
    else if (getSSIDPass != 0 && incomingChar != '\n'){
      password += String(incomingChar);
    }
  }
  //Save SSID and password to EEPROM
  writeStringToEEPROM(0, ssid);
  writeStringToEEPROM(32,password);
}

void writeStringToEEPROM(int addrOffset, const String &strToWrite)
{
  byte len = strToWrite.length();
  EEPROM.write(addrOffset, len);
  for (int i = 0; i < len; i++)
  {
    EEPROM.write(addrOffset + 1 + i, strToWrite[i]);
    EEPROM.commit();
  }
}

String readStringFromEEPROM(int addrOffset)
{
  int newStrLen = EEPROM.read(addrOffset);
  char str[newStrLen + 1];
    for (int i = 0; i < newStrLen; i++){
      str[i] = EEPROM.read(addrOffset + 1 + i);
    }
  str[newStrLen] = '\0';
  return String(str);
}

void stringToArray(String &string, const char *delimiter, int *elementCount, char *char_array[100]){
  char buf[1000];
  string.toCharArray(buf,1000);
  char *p = buf;
  char *str;
  int i = 0;
  while ((str = strtok_r(p, delimiter, &p)) != NULL){
    char_array[i] = strdup(str);
    i++;
  }
  *elementCount = i;
}

//Motor movement function. Differentiated into 2 for each function
//One with a long integer argument to move motor to certain steps
//Other one just move motor with certain speed until next sensor readings
void goForward(){
  leftMotor.setSpeed(spd);
  rightMotor.setSpeed(spd);
  leftMotor.runSpeed();
  rightMotor.runSpeed();
}

void goForward(long steps){
  leftMotor.moveTo(leftMotor.currentPosition()+steps);
  rightMotor.moveTo(rightMotor.currentPosition()+steps);
  leftMotor.setSpeed(spd);
  rightMotor.setSpeed(spd);
  while (leftMotor.distanceToGo() != 0){
    leftMotor.runSpeedToPosition();
    rightMotor.runSpeedToPosition();
  }
}

void goLeft(){
  leftMotor.stop();
  rightMotor.setSpeed(spd);
  rightMotor.runSpeed();
}

void goLeft(long steps){
  rightMotor.moveTo(rightMotor.currentPosition()+steps);
  rightMotor.setSpeed(spd);
  leftMotor.stop();
  while (rightMotor.distanceToGo() != 0){
    rightMotor.runSpeedToPosition();
  }
}

void goRight(){
  leftMotor.setSpeed(spd);
  leftMotor.runSpeed();
  rightMotor.stop();
}

void goRight(long steps){
  rightMotor.stop();
  leftMotor.moveTo(leftMotor.currentPosition()+steps);  
  leftMotor.setSpeed(spd);
  while (leftMotor.distanceToGo() != 0){
    leftMotor.runSpeedToPosition();
  }
}

void pivotLeft(){
  do{
    leftMotor.setSpeed(-spd);
    rightMotor.setSpeed(spd+70);
    leftMotor.runSpeed();
    rightMotor.runSpeed();
    readSensors();
    Serial.println(state);
    if (!right2){
      state = 8;
      break;
    }
  }while (right1);
}

void pivotLeft(long steps){
  leftMotor.moveTo(leftMotor.currentPosition()-steps);
  rightMotor.moveTo(rightMotor.currentPosition()+steps);
  leftMotor.setSpeed(-spd);
  rightMotor.setSpeed(spd+50);
  while (leftMotor.distanceToGo() != 0){
    leftMotor.runSpeedToPosition();
    rightMotor.runSpeedToPosition();
  }
}

void pivotRight(){
  do{
    leftMotor.setSpeed(spd+70);
    rightMotor.setSpeed(-spd+20);
    leftMotor.runSpeed();
    rightMotor.runSpeed();
    readSensors();
    Serial.println(state);
    if (!left2){
      state = 8;
      break;
    }
  }while (left1);
}

void pivotRight(long steps){
  leftMotor.moveTo(leftMotor.currentPosition()+steps);
  rightMotor.moveTo(rightMotor.currentPosition()-steps);
  leftMotor.setSpeed(spd+50);
  rightMotor.setSpeed(-spd);
  while (leftMotor.distanceToGo() != 0){
    leftMotor.runSpeedToPosition();
    rightMotor.runSpeedToPosition();
  }
}


void brake(){
  leftMotor.stop();
  rightMotor.stop();
}

void turnAround(){
  leftMotor.moveTo(leftMotor.currentPosition()+520);
  rightMotor.moveTo(rightMotor.currentPosition()-520);
  leftMotor.setSpeed(spd);
  rightMotor.setSpeed(-spd);
  while (leftMotor.distanceToGo() != 0){
    leftMotor.runSpeedToPosition();
    rightMotor.runSpeedToPosition();
  }
}

//If more rooms needed, this function needs to be re-adjusted to
//match the sequence from DFPlayer
void playRoomSound(int trackNumber){
  myDFPlayer.play(12);
  delay(500);
  myDFPlayer.play(trackNumber);
  delay(1000);
  myDFPlayer.play(11);
  delay(3500);
}

Carrie App Source Code

Credits

Feroz Fernando

Feroz Fernando

3 projects • 3 followers
Electrical engineering student
Andre S. Manurung

Andre S. Manurung

1 project • 1 follower

Comments