Annu JohnsAchinth P MSapnil MAbhinav Krishna
Published

Bolt :The quadruped robotic dog

BOLT is a modular quadruped robotic scout that enters hazardous environments first

ExpertWork in progressOver 10 days528
Bolt :The quadruped robotic dog

Things used in this project

Hardware components

Raspberry Pi 5
Raspberry Pi 5
×1
Eaglepower 90 kv bldc motor
×1
mks odrive mini
×1
Grove - Temperature, Humidity, Pressure and Gas Sensor (BME680)
Seeed Studio Grove - Temperature, Humidity, Pressure and Gas Sensor (BME680)
×1
Grove - VOC and eCO2 Gas Sensor SGP30
Seeed Studio Grove - VOC and eCO2 Gas Sensor SGP30
×1
Grove - Oxygen Sensor(ME2-O2-Ф20)
Seeed Studio Grove - Oxygen Sensor(ME2-O2-Ф20)
×1
Grove - Multichannel Gas Sensor v2
Seeed Studio Grove - Multichannel Gas Sensor v2
×1
Grove - I2C Hub
Seeed Studio Grove - I2C Hub
×1
OAK D PRO
×1
LIDAR
×1
Seeed Studio SenseCAP Card Tracker T1000-E for Meshtastic
×1
Seeed Studio Hazard Response Mission Pack - All-in-one Compact Kit, Open-source AIoT Solution, Meshtastic-powered, No/Low-code
×1
Seeed Studio Wio Tracker L1 Lite
×1

Software apps and online services

Arduino IDE
Arduino IDE
MQTT
MQTT
Fusion
Autodesk Fusion
Node-RED
Node-RED
bambu labs
VS Code
Microsoft VS Code
Robot Operating System
ROS Robot Operating System

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Custom parts and enclosures

BOLT FINAL FILE

Schematics

Block Diagram

Code

BOLT FINAL CODE

Arduino
#include <FlexCAN_T4.h>
#include <circular_buffer.h>
#include <imxrt_flexcan.h>
#include <isotp.h>
#include <isotp_server.h>
#include <kinetis_flexcan.h>

// BOLT Quadruped Workout Program
#include <Arduino.h>
#include <ODriveTeensyCAN.h>
#include <LiquidCrystal_I2C.h>
#include <RC_Reciever.h>
#include <QIK.h>

ODriveTeensyCAN odriveCAN(250000);       // CAN bus object
LiquidCrystal_I2C lcd(0x27, 16, 2);      // 16x2 LCD object
RC_Reciever RC(0, 1, 2, 3, 4, 5, 6, 7);  // pwm pins for each channel
QIK QIK(94.5, 180.0, 180.0);             // QIK object: lengths a, b, and c in mm

// CAN Bus Variables
CAN_message_t inMsg;                     
EncoderEstimatesMsg_t encoderEstimates;  

// QIK Variables
float normX = 110, normYF = -30, normYB = -100, normZ = 300;  
float GR = 9.0;                                               
// Absolute value of actuator angle offsets at the physical limits (in degrees) { abab, hip, knee}
float offset[4][3] = { { 120.0, 0, 47.449 },       // FL leg
                       { 120.0, 0, 58.851 },       // FR leg
                       { 120.0, 180, 298.149 },    // BL leg
                       { 120.0, 180, 307.388 } };  // BR leg

// Direction of increasing position according to the Odrive (CW = +1, CCW = -1) { abab, hip, knee}
float dir[4][3] = { { 1, 1, 1 },     // FL leg
                    { -1, -1, -1 },  // FR leg
                    { -1, -1, -1 },  // BL leg
                    { 1, 1, 1 } };   // BR leg

int mode = 0;  

void setup() {
  Serial.begin(115200);  
  RC.begin(-10, 10);     
  lcd.init();
  lcd.clear();
  lcd.backlight();
}

void loop() {
  while (RC.getChn(5) == 0) {
    lcd.setCursor(5, 0);
    lcd.print("BOLT");
    lcd.setCursor(3, 1);
    lcd.print("QUADRUPED");
    while (RC.getChn(5) == 0) {}
  }
  lcd.clear();

  while (RC.getChn(5) == 1) {
    if (RC.getChn(6)) {              
      while (RC.getChn(6)) {}        
      mode += (mode == 3 ? -3 : 1);  
      lcd.clear();
    }
    lcd.setCursor(2, 0);
    lcd.print("-SELECT-");
    printModeName(1);  
  }
  lcd.clear();

  if (RC.getChn(5) == -1) {
    if (confirmSelection()) {
      switch (mode) {
        case 0: REBOOT();    break;
        case 1: TELEOP();    break;
        case 2: IDLE();      break;
        case 3: CALIBRATE(); break;
      }
    }
    while (RC.getChn(5) == -1) {
      printModeName(0);  
      lcd.setCursor(3, 1);
      lcd.print("CANCELED");
    }
  }
  lcd.clear();
}

void setParam(char param, float abad, float hip, float knee) {
  float vals[3] = { abad, hip, knee };  
  for (int i = 0; i < 12; i++) {         
    switch (param) {
      case 'v': odriveCAN.SetLimits(i, vals[i % 3], 36); break;
      case 'p': odriveCAN.SetPositionGain(i, vals[i % 3]); break;
      case 'd': odriveCAN.SetVelocityGains(i, vals[i % 3], 0); break;
    }
  }
}

// Renamed from moveTOPS to moveBOLT
void moveBOLT(float X0, float Y0, float Z0,    
              float X1, float Y1, float Z1,    
              float X2, float Y2, float Z2,    
              float X3, float Y3, float Z3) {  
  float relPos;                                
  float pos[4][3] = { { X0, Y0, Z0 },    
                      { X1, Y1, Z1 },    
                      { X2, Y2, Z2 },    
                      { X3, Y3, Z3 } };  
  for (int i = 0; i < 4; i++) {
    for (int j = 0; j < 3; j++) {
      relPos = QIK.getTheta(j, pos[i][0], pos[i][1], pos[i][2]) - offset[i][j];
      relPos = fabs(relPos / 360.0 * GR) * dir[i][j];
      odriveCAN.SetPosition(i * 3 + j, relPos);
    }
  }
}

void step(char direction) {
  normYF = (direction == 'F') ? -50 : -30;
  normYB = (direction == 'B') ? -50 : (direction == 'F' ? -110 : -100);
  
  // Renamed local variable to 'valOffset' to avoid conflict with global 'offset' array
  int valOffset = (direction == 'F') ? 60 : (direction == 'B' ? -60 : (direction == 'R' ? 50 : -50));
  int offsetX = 0, offsetY = 0;
  int time = 60;

  moveBOLT(normX, normYF, normZ - abs(valOffset), 
           normX, normYF, normZ, 
           normX, normYB, normZ, 
           normX, normYB, normZ - abs(valOffset));
  delay(time);

  offsetX = (direction == 'R' || direction == 'L' || direction == 'C') ? valOffset : 0;
  offsetY = (direction == 'F' || direction == 'B') ? valOffset : 0;

  moveBOLT(normX - offsetX, normYF + offsetY, normZ - abs(valOffset), 
           normX, normYF, normZ, 
           normX, normYB, normZ, 
           normX + offsetX * (direction == 'C' ? -1 : 1), normYB + offsetY, normZ - abs(valOffset));
  delay(time);

  moveBOLT(normX - offsetX, normYF + offsetY, normZ, 
           normX, normYF, normZ, 
           normX, normYB, normZ, 
           normX + offsetX * (direction == 'C' ? -1 : 1), normYB + offsetY, normZ);
  delay(time);

  moveBOLT(normX, normYF, normZ, 
           normX, normYF, normZ - abs(valOffset), 
           normX, normYB, normZ - abs(valOffset), 
           normX, normYB, normZ);
  delay(time);

  moveBOLT(normX, normYF, normZ, 
           normX + offsetX * (direction == 'C' ? -1 : 1), normYF + offsetY, normZ - abs(valOffset), 
           normX - offsetX, normYB + offsetY, normZ - abs(valOffset), 
           normX, normYB, normZ);
  delay(time);

  moveBOLT(normX, normYF, normZ, 
           normX + offsetX * (direction == 'C' ? -1 : 1), normYF + offsetY, normZ, 
           normX - offsetX, normYB + offsetY, normZ, 
           normX, normYB, normZ);
  delay(time);
}

void inPlace() {
  normYF = -30; normYB = -100; normZ = 300;
  moveBOLT(normX, normYF, normZ - 80, normX, normYF, normZ, normX, normYB, normZ, normX, normYB, normZ - 80);
  delay(95);
  moveBOLT(normX, normYF, normZ, normX, normYF, normZ, normX, normYB, normZ, normX, normYB, normZ);
  delay(95);
  moveBOLT(normX, normYF, normZ, normX, normYF, normZ - 80, normX, normYB, normZ - 80, normX, normYB, normZ);
  delay(95);
  moveBOLT(normX, normYF, normZ, normX, normYF, normZ, normX, normYB, normZ, normX, normYB, normZ);
  delay(95);
}

void printModeName(int row) {
  lcd.setCursor(0, row);
  switch (mode) {
    case 0: lcd.print("MODE: REBOOT"); break;
    case 1: lcd.print("MODE: TELEOP"); break;
    case 2: lcd.print("MODE: IDLE");   break;
    case 3: lcd.print("MODE: CALIBRATE"); break;
  }
}

bool confirmSelection() {
  int count = 2; int timeI;
  while (RC.getChn(5) == -1) {
    if (RC.getChn(6)) {
      timeI = millis();
      while (RC.getChn(6) && millis() - timeI < 1000) {}
      lcd.clear();
      if (millis() - timeI >= 1000) return (count % 2);
      else count++;
    }
    printModeName(0);
    lcd.setCursor(0, 1);
    if (count % 2) lcd.print("ENTER?  NO *YES*");
    else lcd.print("ENTER? *NO* YES");
  }
  return false;
}

void REBOOT() {
  IDLE(); 
  printModeName(0);
  while (RC.getChn(6)) {}
  int leg = 0;
  String legName[4] = { "FRONT LEFT", "FRONT RIGHT", "BACK LEFT", "BACK RIGHT" };
  while (RC.getChn(5) == -1) {
    if (RC.getChn(6)) {
      int timeI = millis();
      while (RC.getChn(6) && millis() - timeI < 1000) {}
      lcd.clear();
      if (millis() - timeI >= 1000) {
        lcd.setCursor(0, 0); lcd.print("LEG: "); lcd.print(legName[leg]);
        lcd.setCursor(2, 1); lcd.print("REBOOTING...");
        for (int i = leg * 3; i < leg * 3 + 3; i++) odriveCAN.RebootOdrive(i);
        delay(3000);
        lcd.clear(); lcd.setCursor(4, 0); lcd.print("FINISHED");
        delay(1000);
      } else {
        leg = (leg + 1) % 4;
      }
      lcd.clear();
    }
    printModeName(0);
    lcd.setCursor(0, 1); lcd.print("LEG: "); lcd.print(legName[leg]);
  }
}

void TELEOP() {
  for (int i = 0; i < 12; i++) {
    odriveCAN.RunState(i, 8);
    odriveCAN.SetControllerModes(i, 3, 1);
  }
  setParam('v', 33, 33, 33);
  setParam('p', 60, 60, 60);
  moveBOLT(normX, normYF, normZ, normX, normYF, normZ, normX, normYB, normZ, normX, normYB, normZ);

  while (RC.getChn(5) == -1) {
    if (RC.getChn(7) == 1) {
      lcd.clear(); printModeName(0); lcd.setCursor(0, 1); lcd.print("NAVIGATE");
      int reset = 0;
      while (RC.getChn(7) == 1 && RC.getChn(5) == -1) {
        if (RC.getChn(2) >= 7)  { reset = 1; step('F'); }
        if (RC.getChn(2) <= -7) { reset = 1; step('B'); }
        if (RC.getChn(1) >= 7)  { reset = 1; step('R'); }
        if (RC.getChn(1) <= -7) { reset = 1; step('L'); }
        if (RC.getChn(4) >= 7)  { reset = 1; step('C'); }
        if (RC.getChn(6))       { inPlace(); }
        if (reset) {
          moveBOLT(normX, normYF, normZ, normX, normYF, normZ, normX, normYB, normZ, normX, normYB, normZ);
          reset = 0;
        }
      }
    }
    // Note: Other TELEOP sub-modes like Inverse Kinematics would also call moveBOLT() here
  }
}

void IDLE() {
  printModeName(0);
  for (int i = 0; i < 12; i++) odriveCAN.RunState(i, 1);
  while (RC.getChn(5) == -1 && mode == 2) {}
}

void CALIBRATE() {
  IDLE();
  printModeName(0);
  while (RC.getChn(6)) {}
  int leg = 0;
  String legName[4] = { "FRONT LEFT", "FRONT RIGHT", "BACK LEFT", "BACK RIGHT" };
  while (RC.getChn(5) == -1) {
    if (RC.getChn(6)) {
      int timeI = millis();
      while (RC.getChn(6) && millis() - timeI < 1000) {}
      lcd.clear();
      if (millis() - timeI >= 1000) {
        lcd.setCursor(0, 0); lcd.print("LEG: "); lcd.print(legName[leg]);
        lcd.setCursor(2, 1); lcd.print("CALIBRATING...");
        for (int i = leg * 3; i < leg * 3 + 3; i++) odriveCAN.RunState(i, 3);
        delay(15000);
        lcd.clear(); lcd.setCursor(4, 0); lcd.print("FINISHED");
        delay(1000);
      } else {
        leg = (leg + 1) % 4;
      }
      lcd.clear();
    }
    printModeName(0);
    lcd.setCursor(0, 1); lcd.print("LEG: "); lcd.print(legName[leg]);
  }
}

Credits

Annu Johns
1 project • 2 followers
ECE student building real-world IoT and embedded solutions, bridging hardware with scalable and reliable software systems.
Achinth P M
0 projects • 3 followers
Sapnil M
1 project • 8 followers
Iot enthusiast and always willing and ready to work on new and exciting projects
Abhinav Krishna
9 projects • 57 followers
Maker | IoT Enthusiast | Electronics hobbyist

Comments