Skip to content

Final Project

Project Silde :

Project Video

MY FINAL PROJECT IDEA

This week I worked on defining my final project idea and started to getting used to the documentation process. The aim of this project is to simplify internal logistics and increase operational efficiency in industrial or creative environments. By using an automated courier robot to transport tools, we can significantly reduce the time lost by employees who need to move to fetch or return tools. This solution could also contribute to better organization of the workspace, reducing clutter caused by poorly stored or lost tools.

The robot will be equipped with sensors and a line-tracking navigation system, which will follow marked lines on the floor to move autonomously within the work environment. It will be programmed to respond to real-time requests through an intuitive user interface. The robot’s carrying capacity must be adapted to the tools commonly used in the relevant sector.

Project Name: MecaMate

Objective: Develop an autonomous robot capable of following floor lines and executing navigation commands to transport parcels between various points in an industrial, port, or commercial environment.

Mateials Required

  • Chassis and wheels
  • DC motors and gearbox
  • Adeept 3-CH line sensor
  • ToF VL53L1X sensor
  • XIAO ESP32C3 (INPUT BOARD) and RP2040 (OUTPUT BOARD) development boards
  • Batteries and energy management systems
  • Communication components (Wi-Fi)
  • L293D drivers (x2) and SN74HC595 shift register

Part 1 : Mechanical Structure

Gearbox

CAD Gearbox

In order to reduce the motor Speed and increase the motor torque, because Mecamate to work need more torque than speed, I decided to design and prnt my own Gearbox.

The Type of Gearbox I choose is Planetary Gearbox because of they precision and efficiency. Here’s is a 1:16 ratio.

I used SolidWorks to design it, as below :

A small Animation to show an explode view of my Gearbox Structure :

Printing

You have to notice that there were a lot of test before coming to an optimal and functional version of the Gearbox.

The Printing parameters are really important here to have an working Gearbox fully functionnal.

Here’s How one mounted stage of the Planetary Gearbox look like after printing !!

Testing

After testing with one gearbox motor and working fine we can now validate the good parameter and start producing the others gearboxes !!!

Other Gearboxe printed parts :

All mounted on motors :

NB:

  • For each Gearbox you will need some Bearings the references are in the design files attached below.
  • To connect the motor shaft to the gearbox shaft I print a Hole which a bit smaller than the motor shaft and then use a Hot Air gun with glue to fix the motor to the Gearbox.
  • Printing parameters with Ultimaker Cura Slicer for the Gear parts of the Gearboxes have to be set to Super Quality and fill at 100%;
  • All the shaft for the motor part and for the wheel have to be printed in an Horizontal position, to make them more solid and do not break.

Design Files

Chassis

CAD Chassis

Robust design in fusion 360

In this part you have to pay attention on the system Integration in order to prepare the right space for each component in the chassis, it’s very very important.

You can refere to the image in the System integration week for the images, were you can see that exported all the different component in the original design file in order to cdefine their location and make the holes if necessary to help an easy fixed parts on the robot chassis and assembly once mounting the final robot.

Here’s the link System Integration.

Here’s an Assembly Chassis CAD animation, to show you how in put each component and mounte them on the chassis

Chassis and mounting parts using 3D printing, Laser cut and CNC milling.

  • 3D Printing :

  • Laser Cut :

  • CNC Milling :

Design Files

Wheels

Four driven wheels with good grip for following floor lines, so I opted to make some arrow patterns on the wheels surface in order to have a good adhesion with the ground.

NB

  • The good Way to print wheels, is to use TPU Filament bcause it offer the good properties for adhesion but in this project i’m not using it because it’s unavailable in my lab, it’s the reason why i used arrow patterns to help PLA printed wheels be adhesive. It’s one of the enhancement I have to do on the project.

Print results !!

I also design and print a wheel blocker to maintain the wheel on the gearbox output shaft.

Here’s a photo of the wheels successfully mounted on the robot :

Design Files

Motors

Four DC 12V motors (AUTOTOOLHOME MODEL:JYCRS390H). For each motors i made a Motor Holder :

here’s the 3D Design part :

  • Motor Holder Bottom part

  • Motor Holder Top Part :

  • I also designed a Gearbox Holder :

Image Holders fully integrated in the robot :

Here are the differents files :

Part 2 : Development Boards:

Input Board

  • XIAO ESP32C3 (INPUT BOARD) for receiving information via Wi-Fi from the mobile application, managing sensors, and sending commands to the OUTPUT BOARD.
  • Line Sensor: Adeept 3-CH tracking module, integrating three sensors in one.
  • Obstacle Sensor: ToF VL53L1X sensor for obstacle detection.

You can refere to my Input Devices week assignement to see more informations about it.

Output Board

  • XIAO RP2040 (OUTPUT BOARD) for controlling the motors and LEDs.

Part 3 : Communication and Control

Mobile Application

Developed with MIT App Inventor to enter commands and find the shortest path using a path-finding algorithm.

  • Communication Protocol: Wi-Fi for communication between the mobile application and the robot.
  • Inter-communication: I2C between the ESP32C3 (INPUT BOARD) and RP2040 (OUTPUT BOARD) boards.

MecaMate Control App Demo !

Test with the robot Input board and it works !!! The App successfully send the command array to the input board via Wifi !

You can refere to my Interface and application programming week assignement to see more informations about it.

Command System

Navigation Algorithm: - The mobile application generates a command array (e.g., go straight, turn left, etc.) based on the shortest path. - The robot uses the line sensor to follow the instructions in the array, using a node logic (e.g., execute the instruction as long as one of the sensors detects a line, if two or three sensors detect a line, stop and execute the next instruction).

Part 4 : Final Assembly and Testing

With all this work my OUTPUT Board isn’t working ! And paased a long time looking for the error in the circuit once I find that :

  • First L293D are not enough powerfull to control my 1A motors;
  • Second, one of the two L293D drivers I have isn’t working because it has been destroyed by a mistake;

But good news, actually I test my mortors with the L298N Driver and it actually working, so i’m actually working on other different OUTPUT Board than the one i made in order to integrate the L298N drivers in the circuit to finish my MecaMate Robot.

here is a video of the test :

Here’s my input board final code :

#include <Wire.h>
#include <WiFi.h>
#include <Adafruit_VL53L0X.h>
#include <Adafruit_NeoPixel.h>
#include <PubSubClient.h> // Bibliothèque pour MQTT

#define LEFT_SENSOR_PIN 2
#define CENTER_SENSOR_PIN 3
#define RIGHT_SENSOR_PIN 4
#define I2C_SLAVE_ADDRESS 9

// LED ring parameters
#define LED_PIN 6
#define NUM_LEDS 24

Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);

const char* ssid = "********"; 
const char* password = "***********"; 
WiFiServer server(80);

const char* mqttServer = "broker.hivemq.com"; // Serveur MQTT (par exemple, HiveMQ public broker)
const char* mqttUserName = "";  // Laisser vide si pas d'authentification
const char* mqttPwd = "";       // Laisser vide si pas d'authentification
const char* clientID = "ESP32Client";
const char* topic = "travel"; // Topic pour recevoir le chemin

WiFiClient espClient;
PubSubClient client(espClient);

// Initialiser le capteur VL53L0X
Adafruit_VL53L0X lox = Adafruit_VL53L0X();
#define VL53L0X_ADDRESS 0x29

String path = ""; // Chemin à suivre, reçu via MQTT
unsigned long previousMillis = 0;
unsigned long pathMillis = 0;
unsigned long interval = 1000; // Intervalle de 1 seconde entre chaque étape du chemin
int currentStep = 0;

void callback(char* topic, byte* payload, unsigned int length) {
  String message;
  for (unsigned int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.print("Message arrived in topic: ");
  Serial.println(topic);
  Serial.print("Message: ");
  Serial.println(message);

  path = message;
  followPath();
}

void setup() {
  Serial.begin(9600);
  pinMode(LEFT_SENSOR_PIN, INPUT);
  pinMode(CENTER_SENSOR_PIN, INPUT);
  pinMode(RIGHT_SENSOR_PIN, INPUT);

  strip.begin();
  strip.show(); // Initialize all pixels to 'off'

  showConnectingAnimation();
  setupWiFi();
  setupMQTT();
  setupI2C();
  setupDistanceSensor();
}

void setupWiFi() {
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi...");
  showConnectingAnimation();
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  showConnectedAnimation();
  Serial.println();
  Serial.println("Connected to WiFi");
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  unsigned long currentMillis = millis();

  if (currentMillis - pathMillis >= interval && path.length() > 0) {
    pathMillis = currentMillis;
    followPathStep();
  }

  readLineSensors(); // Lire les capteurs de ligne
  readObstacleSensor(); // Lire le capteur de distance
  handleAnimations(); // Gérer les animations LED
}

void setupMQTT() {
  client.setServer(mqttServer, 1883);
  client.setCallback(callback);
  reconnect();
}

void reconnect() {
  while (!client.connected()) {
    if (client.connect(clientID, mqttUserName, mqttPwd)) {
      Serial.println("Connected to MQTT");
      client.subscribe(topic);
      Serial.println("Subscribed to topic");
    } else {
      Serial.print("Failed to connect, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void setupI2C() {
  Wire.begin(); // Initialiser en tant que maître
  Serial.println("I2C initialized");
}

void setupDistanceSensor() {
  if (!lox.begin()) {
    Serial.println(F("Echec de l'initialisation du capteur VL53L0X"));
    while (1);
  }
  Serial.println(F("Capteur VL53L0X initialisé"));
}

void followPath() {
  showDeliveryInProgressColor();
  pathMillis = millis(); // Réinitialiser le compteur de temps pour suivre le chemin
}

void followPathStep() {
  if (currentStep < path.length()) {
    char step = path.charAt(currentStep);
    int command = determineCommand(step);
    sendCommand(command);
    currentStep++;
  } else {
    showDeliveryCompleteColor();
    path = ""; // Réinitialiser le chemin une fois terminé
  }
}

int readLineSensors() {
  int leftSensor = digitalRead(LEFT_SENSOR_PIN);
  int centerSensor = digitalRead(CENTER_SENSOR_PIN);
  int rightSensor = digitalRead(RIGHT_SENSOR_PIN);

  if (leftSensor == HIGH && centerSensor == HIGH && rightSensor == HIGH) {
    return 'P'; // Point d'arrêt détecté
  }
  if (centerSensor == HIGH && leftSensor == LOW && rightSensor == LOW) {
    return 'F'; // Le robot est sur la ligne
  }
  if (leftSensor == HIGH && centerSensor == LOW && rightSensor == LOW) {
    return 'L'; // Le robot doit tourner à gauche
  }
  if (rightSensor == HIGH && centerSensor == LOW && leftSensor == LOW) {
    return 'R'; // Le robot doit tourner à droite
  }
  return 'S'; // Par défaut, arrêter si aucun capteur ne détecte la ligne
}

int readObstacleSensor() {
  VL53L0X_RangingMeasurementData_t measure;

  lox.rangingTest(&measure, true);
  if (measure.RangeStatus != 4) {  
    Serial.print("Distance (mm) : ");
    Serial.println(measure.RangeMilliMeter);
    if (measure.RangeMilliMeter < 200) return 'S'; // Obstacle détecté, arrêt
  } else {
    Serial.println("Hors de portée");
  }
  return -1; // Pas d'obstacle
}

int determineCommand(char step) {
  int obstacleCommand = readObstacleSensor();
  if (obstacleCommand != -1) {
    return obstacleCommand; // Priorité à l'obstacle
  }

  int lineCommand = readLineSensors();
  if (lineCommand != 'S') {
    if (lineCommand == 'P') {
      if (step == 'P') {
        return 'S';
      } else {
        return 'F';
      }
    }
    return lineCommand;
  }

  switch (step) {
    case 'A':
      return 'R';
    case 'B':
      return 'L';
    case 'C':
      return 'F';
    case 'D':
      return 'S';
    default:
      return 'S';
  }
}

void sendCommand(int command) {
  Wire.beginTransmission(I2C_SLAVE_ADDRESS); 
  Wire.write(command);                       
  Wire.endTransmission();

  Serial.print("Command sent: ");
  Serial.println((char)command);
}

void handleAnimations() {
  // Fonction pour gérer les animations LED de manière non bloquante
  // Par exemple, si une animation est en cours, continuer à l'exécuter étape par étape
  // Ajouter le code de gestion des animations ici
}

// Animation de connexion
void showConnectingAnimation() {
  for(int i=0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, strip.Color(0, 0, 255)); // Bleu
    strip.show();
    delay(50);
  }
  strip.clear();
  strip.show();
}

// Animation de connexion réussie
void showConnectedAnimation() {
  for(int i=0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, strip.Color(0, 255, 0)); // Vert
  }
  strip.show();
}

// Animation de réception du chemin
void showReceivingPathAnimation() {
  for(int i=0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, strip.Color(255, 255, 0)); // Jaune
    strip.show();
    delay(50);
  }
  strip.clear();
  strip.show();
}

// Couleur pendant la livraison
void showDeliveryInProgressColor() {
  for(int i=0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, strip.Color(0, 0, 255)); // Bleu
  }
  strip.show();
}

// Couleur après la livraison
void showDeliveryCompleteColor() {
  for(int i=0; i < strip.numPixels(); i++) {
    strip.setPixelColor(i, strip.Color(255, 0, 0)); // Rouge
  }
  strip.show();
}

Here’s my OUTPUT Board code for L293D drivers with the Shftregister :

#include <Wire.h>

#define PIN_DS 10   //pin 14  75HC595
#define PIN_STCP 9  //pin 12  75HC595
#define PIN_SHCP 8  //pin 11  75HC595
#define ENABLE_A 0
//#define ENABLE_B 1
#define ENABLE_C 2
//#define ENABLE_D 3

#define I2C_SLAVE_ADDRESS 9
/*

void executeCommand(char command) {
  switch (command) {
    case 'F': // Avancer
      MOVE_FORWARD(50);
      break;
    case 'B': // Reculer
      MOVE_BACKWARD(50);
      break;
    case 'L': // Tourner à gauche
      TURN_LEFT(50);
      break;
    case 'R': // Tourner à droite
      TURN_RIGHT(50);
      break;
    case 'S': // Stop
      STOP_MECAMATE();
      break;
    default:
      STOP_MECAMATE();
      break;
  }
}

void receiveEvent(int howMany) {
  while (Wire.available()) {
    char command = Wire.read();
    executeCommand(command);
  }
}
*/

#define numberOf74hc595 1

// nombre total de pin de registre a decalage
#define numOfRegisterPins numberOf74hc595 * 8

boolean registers[numOfRegisterPins];

void setup() {
  pinMode(PIN_DS, OUTPUT);
  pinMode(PIN_STCP, OUTPUT);
  pinMode(PIN_SHCP, OUTPUT);
  pinMode(ENABLE_A, OUTPUT);
  pinMode(ENABLE_B, OUTPUT);
  pinMode(ENABLE_C, OUTPUT);
  pinMode(ENABLE_D, OUTPUT);

  //Wire.begin(I2C_SLAVE_ADDRESS);
  //Wire.onReceive(receiveEvent);

  Serial.begin(9600);
  clearRegisters();
  // on applique les valeurs au registre a decalage
  writeRegisters();
}

// Mettre toutes les valeurs a 0 pour le resigtre
void clearRegisters() {
  for (int i = numOfRegisterPins - 1; i >= 0; i--) {
    registers[i] = LOW;
  }
}

void writeRegisters(){

 // Tant que LOW les modifications ne seront pas affectés
  digitalWrite(PIN_STCP, LOW);

 // boucle pour affecter chaque pin des 74hc595
  for(int i = numOfRegisterPins - 1; i >=  0; i--){

    //doit etre a l'etat bas pour changer de colonne plus tard
    digitalWrite(PIN_SHCP, LOW);

    // recuperation de la valeur dans le tableau registers
    int val = registers[i];

    //affecte la valeur sur le pin DS correspondant a un pin du 74hc595
    digitalWrite(PIN_DS, val);

    //colonne suivante
    digitalWrite(PIN_SHCP, HIGH);
  }
  //applique toutes les valeurs au 74hc595
  digitalWrite(PIN_STCP, HIGH);

}

// enregistre une valeur pour un registre etat haut ou bas
void setRegisterPin(int index, int value){
  registers[index] = value;
}

void MOVE_FORWARD(int speed) {
  setRegisterPin(0, HIGH);
  setRegisterPin(1, LOW);
  // setRegisterPin(2, HIGH);
  // setRegisterPin(3, LOW);
  // setRegisterPin(4, HIGH);
  // setRegisterPin(5, LOW);
  // setRegisterPin(6, HIGH);
  // setRegisterPin(7, LOW);
  analogWrite(ENABLE_A, speed);
  analogWrite(ENABLE_B, speed);
  analogWrite(ENABLE_C, speed);
  analogWrite(ENABLE_D, speed);
// appelle la fonction pour appliquer les valeurs
  writeRegisters();  

}

void MOVE_BACKWARD(int speed_b) {
  digitalWrite(PIN_STCP, LOW);
  shiftOut(PIN_DS, PIN_SHCP, LSBFIRST, 0B01010101);
  analogWrite(ENABLE_A, speed_b);
  analogWrite(ENABLE_B, speed_b);
  analogWrite(ENABLE_C, speed_b);
  analogWrite(ENABLE_D, speed_b);
  digitalWrite(PIN_STCP, HIGH);
}

void TURN_RIGHT(int speed_r) {
  digitalWrite(PIN_STCP, LOW);
  shiftOut(PIN_DS, PIN_SHCP, LSBFIRST, 0B01100110);
  analogWrite(ENABLE_A, speed_r);
  analogWrite(ENABLE_B, speed_r);
  analogWrite(ENABLE_C, speed_r);
  analogWrite(ENABLE_D, speed_r);
  digitalWrite(PIN_STCP, HIGH);
}

void TURN_LEFT(int speed_b) {
  digitalWrite(PIN_STCP, LOW);
  shiftOut(PIN_DS, PIN_SHCP, LSBFIRST, 0B10011001);
  analogWrite(ENABLE_A, speed_b);
  analogWrite(ENABLE_B, speed_b);
  analogWrite(ENABLE_C, speed_b);
  analogWrite(ENABLE_D, speed_b);
  digitalWrite(PIN_STCP, HIGH);
}

void STOP_MECAMATE() {
  digitalWrite(PIN_STCP, LOW);
  shiftOut(PIN_DS, PIN_SHCP, LSBFIRST, 0B00000000);
  digitalWrite(PIN_STCP, HIGH);
}

void loop() {
  MOVE_FORWARD(50);
}

Last update: July 23, 2024