Close
0%
0%

Ghost Toddler: esp32 FPV pan tilt Power Wheels

I use an RC remote and receiver, an esp32, high-current motor drivers, servos, an FPV camera, and a little propane.

Public Chat
Similar projects worth following
An ESP32 is the brains.
The ESP32 reads a serial stream with a 32 byte payload from an RC receiver (packaging is called "I-Bus").
The code interprets the payload into the 10 channels (e.g. joysticks and switches and stuff) from the transmitter.
The Esp32 outputs to:
-two BTS7960 dc motor drivers (wheels forward or reverse)
-one 35kg*cm servo (steering shaft pulley)
-two 25kg*cm servos (skull pan and tilt servos
-one 12V relay controlling a gas solenoid valve
-one 12V relay controlling a mini high-voltage transformer

I put an analog FPV camera in the right eye socket so I could look around with one joystick while I drove with the other joystick… Kind of like legend of Zelda.

The end product: I made a radio controlled power wheels that allowed me to FPV the experience of being the pediatric version of Ghost Rider, all accoutrements accounted for.

This project happened on a dare. I just figured out how to receive/interpret/use the serial stream from an RC receiver on an esp32 using the Arduino IDE (I was never really an RC person, but I was RC curious).

However, I had not yet completed two rites of passage for most maker-types;

1) I had never built anything really robotic, and

2) I had never built anything seriously battery-powered.

Despite this lack of experience, I proclaimed on September 26 that I thought I could do this project and publish a video on it by Halloween. I’m gonna skip straight to the “Build Instructions” part if the reader is curious on how to do this, and I am happy to lay it out.

You can read the instructions and follow along on my full build video as well:

  • 1
    Interpreting an RC receiver serial payload

    Honestly, I saw how relatively inexpensive these transmitter/receiver bundles were, and I saw how much functionality was built into them, and I knew they worked at a longer range than Bluetooth and standard Wi-Fi (I’m not even going near ESPNow but maybe), and I had to screw around with one. 

    I was doing an unrelated project using field oriented control of bushels motors, and I got the remote control to do this, which got me jazzed:

    Each degree of motion (we will call them “channels,” because that is what they are called in this arena) on the transmitter is broadcast to the receiver. On the receiver side, each channel outputs a PWM signal that can directly control something, if it wants to, which we don’t want to do. Instead, we will utilize a different way to use way less wires, and it is easy and satisfying.

    Instead of having a PWM line for each channel fed into the ESP32, this brand of remote has this thing called “I-Bus,” which sounds complicated, and sounds even more complicated upon learning it involves reading a serial stream of bytes from an RC receiver and turning that into meaningful information.

    Luckily, as of October 2024, ChatGPT4o++^2 will be familiar with this, and upon our very detailed prompts which reflect our deep understanding of syntax and semantics of C++ (to be clear, this is sarcasm about my own proficiency), we get a pretty cool class for reading off a second UART stream to the ESP32. It gets better. We even get efficient code so as not to flood the buffer (I don’t know what that means but I’m writing it to sound knowledgeable) that reads one by every time it goes through the loop, scanning for the to start bytes, then reading the remaining 30 bytes that encode the data of every state of every doodad on the transmitter. Here is the class:

    // Include necessary libraries (if any)
    // Note: Since we're defining the class and using it in the same file, no additional includes are necessary.
    
    // Define the IBusReceiver class
    class IBusReceiver {
      public:
        // Constructor
        IBusReceiver(HardwareSerial& serialPort, uint8_t rxPin) : serial(serialPort), rxPin(rxPin) {
          packetIndex = 0;
          packetAvailable = false;
        }
    
        // Initialize the serial port
        void begin(uint32_t baudRate) {
          serial.begin(baudRate, SERIAL_8N1, rxPin, -1);
        }
    
        // Non-blocking function to read IBUS data
        void readIBUSData() {
          while (serial.available()) {
            uint8_t incomingByte = serial.read();
            switch (packetIndex) {
              case 0:
                if (incomingByte == HEADER1) {
                  ibusPacket[packetIndex++] = incomingByte;
                }
                break;
              case 1:
                if (incomingByte == HEADER2) {
                  ibusPacket[packetIndex++] = incomingByte;
                } else {
                  packetIndex = 0;  // Reset if header not matched
                }
                break;
              default:
                ibusPacket[packetIndex++] = incomingByte;
                if (packetIndex == IBUS_PACKET_SIZE) {
                  // Full packet received
                  packetAvailable = true;
                  packetIndex = 0;  // Reset for next packet
                }
                break;
            }
          }
        }
    
        // Function to process the received IBUS packet
        bool processIBUSPacket() {
          if (!packetAvailable) {
            return false;
          }
    
          // Verify checksum before processing
          if (!verifyChecksum()) {
            packetAvailable = false;
            return false;  // Checksum failed, discard packet
          }
    
          // Extract channel data
          camX    = ibusPacket[2 + 0 * 2] | (ibusPacket[3 + 0 * 2] << 8);
          camY    = ibusPacket[2 + 1 * 2] | (ibusPacket[3 + 1 * 2] << 8);
          moveX   = ibusPacket[2 + 2 * 2] | (ibusPacket[3 + 2 * 2] << 8);
          moveY   = ibusPacket[2 + 3 * 2] | (ibusPacket[3 + 3 * 2] << 8);
          switchA = ibusPacket[2 + 4 * 2] | (ibusPacket[3 + 4 * 2] << 8);
          switchB = ibusPacket[2 + 5 * 2] | (ibusPacket[3 + 5 * 2] << 8);
          switchC = ibusPacket[2 + 6 * 2] | (ibusPacket[3 + 6 * 2] << 8);
          switchD = ibusPacket[2 + 7 * 2] | (ibusPacket[3 + 7 * 2] << 8);
          varA    = ibusPacket[2 + 8 * 2] | (ibusPacket[3 + 8 * 2] << 8);
          varB    = ibusPacket[2 + 9 * 2] | (ibusPacket[3 + 9 * 2] << 8);
    
          packetAvailable = false;
          return true;
        }
    
        // Getters for channel values
        int getCamX()    { return camX; }
        int getCamY()    { return camY; }
        int getMoveX()   { return moveX; }
        int getMoveY()   { return moveY; }
        int getSwitchA() { return switchA; }
        int getSwitchB() { return switchB; }
        int getSwitchC() { return switchC; }
        int getSwitchD() { return switchD; }
        int getVarA()    { return varA; }
        int getVarB()    { return varB; }
    
      private:
        HardwareSerial& serial;
        uint8_t rxPin;
    
        static const int IBUS_PACKET_SIZE = 32;
        static const uint8_t HEADER1 = 0x20;
        static const uint8_t HEADER2 = 0x40;
    
        uint8_t ibusPacket[IBUS_PACKET_SIZE];
        int packetIndex;
        bool packetAvailable;
    
        int camX, camY, moveX, moveY, switchA, switchB, switchC, switchD, varA, varB;
    
        // Function to verify the checksum of the IBUS packet
        bool verifyChecksum() {
          uint16_t checksum = 0xFFFF;
          for (int i = 0; i < IBUS_PACKET_SIZE - 2; i++) {
            checksum -= ibusPacket[i];
          }
          uint16_t receivedChecksum = ibusPacket[IBUS_PACKET_SIZE - 2] | (ibusPacket[IBUS_PACKET_SIZE - 1] << 8);
          return checksum == receivedChecksum;
        }
    };
    
    // Create an instance of IBusReceiver
    // For ESP32, Serial2 is typically used for UART communication
    IBusReceiver ibus(Serial2, 16);  // Assuming RX is on GPIO 13
    
    void setup() {
      Serial.begin(115200);  // Initialize Serial Monitor
      ibus.begin(115200);    // Initialize IBUS receiver with baud rate 115200
    
      // Optional: Give some time for serial ports to initialize
      delay(1000);
    }
    
    void loop() {
      // Read IBUS data
      ibus.readIBUSData();
    
      // Process IBUS packet if available
      if (ibus.processIBUSPacket()) {
        // Retrieve the channel values
        int camX    = ibus.getCamX();
        int camY    = ibus.getCamY();
        int moveX   = ibus.getMoveX();
        int moveY   = ibus.getMoveY();
        int switchA = ibus.getSwitchA();
        int switchB = ibus.getSwitchB();
        int switchC = ibus.getSwitchC();
        int switchD = ibus.getSwitchD();
        int varA    = ibus.getVarA();
        int varB    = ibus.getVarB();
    
        // Print the channel values
        Serial.print(2000);
        Serial.print(" ");
        Serial.print(1000);
        Serial.print(" ");
        Serial.print(camX);
        Serial.print(" ");
        Serial.print(camY);
        Serial.print(" ");
        Serial.print(moveX);
        Serial.print(" ");
        Serial.print(moveY);
        Serial.print(" ");
        Serial.print(switchA);
        Serial.print(" ");
        Serial.print(switchB);
        Serial.print(" ");
        Serial.print(switchC);
        Serial.print(" ");
        Serial.print(switchD);
        Serial.print(" ");
        Serial.print(varA);
        Serial.print(" ");
        Serial.print(varB);
        Serial.println("");
      }
    
      // Optional: Add a small delay to prevent flooding the serial monitor
      // delay(10);
    }
    
    

      To be clear, I'm sure this could massively be improved upon, but I have a bunch of more steps to publish.

  • 2
    Connect esp32 to two BTS7960 motor drivers to control wheel dc motors

    We are going to use some somewhat beefy but inexpensive motor drivers, one for each wheel (each of which has its own motor). The BTS7960 is rated up to 43A, which will be more than sufficient unless we live on a steep hill (in which case we would like to hear from us). 

    We will assign two pins to the ESP32 to output the PWM signal using the MCPWM library (apparently it’s like, better than the LEDC library or something for reasons I don’t understand). While we are soldering, we will ask ChatGPT^ei to write up another cpp class allowing us to easily control the drivers from the main loop.

    This code uses the MCPWM library to drive the BTS7960:

    #include "driver/mcpwm.h"
    
    // MyMotor class for controlling the BTS7960 motor driver
    class MyMotor {
    public:
      // Constructor: Initializes MCPWM and sets up pins
      MyMotor(int forwardPin, int reversePin) {
        this->forwardPin = forwardPin;
        this->reversePin = reversePin;
    
        // Initialize MCPWM for forward and reverse pins
        mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, forwardPin); // Initialize forward pin for PWM
        mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0B, reversePin); // Initialize reverse pin for PWM
    
        // Configure MCPWM unit 0, timer 0 for both forward and reverse control
        mcpwm_config_t pwm_config;
        pwm_config.frequency = 1000;     // Set frequency to 1 kHz
        pwm_config.cmpr_a = 0.0;         // Initial duty cycle of PWMxA = 0%
        pwm_config.cmpr_b = 0.0;         // Initial duty cycle of PWMxB = 0%
        pwm_config.counter_mode = MCPWM_UP_COUNTER; // Up counting mode
        pwm_config.duty_mode = MCPWM_DUTY_MODE_0;   // Active high duty
    
        // Initialize MCPWM with the above configuration
        mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &pwm_config);
      }
    
      // Method to set speed and direction: -100 to 100
      void setSpeed(int speed) {
        if (speed < -100) speed = -100;   // Limit speed to minimum -100
        if (speed > 100) speed = 100;     // Limit speed to maximum 100
    
        // Calculate the absolute duty cycle percentage (0 to 100)
        float dutyCycle = abs(speed);
    
        // Forward direction
        if (speed > 0) {
          mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, dutyCycle); // Set duty for forward
          mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_B, 0.0);       // Ensure reverse is 0
          Serial.print("Forward Speed: ");
        }
        // Reverse direction
        else if (speed < 0) {
          mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, 0.0);       // Ensure forward is 0
          mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_B, dutyCycle); // Set duty for reverse
          Serial.print("Reverse Speed: ");
        }
        // Stop
        else {
          mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, 0.0);       // Stop forward
          mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_B, 0.0);       // Stop reverse
          Serial.println("Motor Stopped");
          return;
        }
    
        // Print the set speed to Serial Monitor
        Serial.print(speed);
        Serial.println("%");
      }
    
    private:
      int forwardPin;   // GPIO for forward PWM control
      int reversePin;   // GPIO for reverse PWM control
    };
    
    // Create an instance of the MyMotor class
    MyMotor motor(4, 15);  // Use GPIO 4 for forward, GPIO 15 for reverse
    
    void setup() {
      Serial.begin(115200);
      Serial.println("Starting Motor Test...");
    
      // No need to initialize pins in setup as MyMotor handles this
    }
    
    void loop() {
      // ** FORWARD CYCLE: Ramp up from 0 to 100% **
      for (int speed = 0; speed <= 100; speed += 10) {
        motor.setSpeed(speed);  // Set speed in the forward direction
        delay(500);             // Adjust delay for smoother ramp-up (lower = smoother)
      }
      
      delay(2000);  // Pause at 100% forward
    
      // ** RAMP DOWN to 0 **
      for (int speed = 100; speed >= 0; speed -= 10) {
        motor.setSpeed(speed);  // Ramp down to 0
        delay(500);
      }
      
      delay(2000);  // Pause at 0% (stopped)
    
      // ** REVERSE CYCLE: Ramp up from 0 to -100% **
      for (int speed = 0; speed >= -100; speed -= 10) {
        motor.setSpeed(speed);  // Set speed in the reverse direction
        delay(500);             // Adjust delay for smoother ramp-up (lower = smoother)
      }
    
      delay(2000);  // Pause at 100% reverse
    
      // ** RAMP DOWN to 0 **
      for (int speed = -100; speed <= 0; speed += 10) {
        motor.setSpeed(speed);  // Ramp down to 0
        delay(500);
      }
    
      delay(2000);  // Pause at 0% (stopped)
    }
    
    

    If this works  correctly, it should look something like the first part of this:

  • 3
    The steering pulley

    Dear reader, you’re on your own for the mechanical part of this section; I was entirely out of my depth here. I’m not exaggerating when I say took four iterations of progressively more (poor) aluminum TIG welding to make something rigid where I could have it fully control the steering wheel. I know some smart people hang out here and might have a better idea, please let me know as this was the best that I can come up with (including with advice from people who know what they are talking about).

    Long story short, we will use a 20-tooth GT2 pulley on a 35kg*cm servo to drive a 40-tooth pulley mounted to the driving shaft. It bears stating, Power Wheels do not use elegant mechanisms for their steering

    Once we make our servo-drive-shaft coupling rigid (this involved more welding and fabricating it out tensioner for me, but I hope you understand this better), let's take our current existing code and add include the servo controlling the steering wheel:

    #include "driver/mcpwm.h"
    
    // --- IBusReceiver Class ---
    class IBusReceiver {
      public:
        // Constructor
        IBusReceiver(HardwareSerial& serialPort, uint8_t rxPin) : serial(serialPort), rxPin(rxPin) {
          packetIndex = 0;
          packetAvailable = false;
        }
    
        // Initialize the serial port
        void begin(uint32_t baudRate) {
          serial.begin(baudRate, SERIAL_8N1, rxPin, -1);
        }
    
        // Non-blocking function to read IBUS data
        void readIBUSData() {
          while (serial.available()) {
            uint8_t incomingByte = serial.read();
            switch (packetIndex) {
              case 0:
                if (incomingByte == HEADER1) {
                  ibusPacket[packetIndex++] = incomingByte;
                }
                break;
              case 1:
                if (incomingByte == HEADER2) {
                  ibusPacket[packetIndex++] = incomingByte;
                } else {
                  packetIndex = 0;  // Reset if header not matched
                }
                break;
              default:
                ibusPacket[packetIndex++] = incomingByte;
                if (packetIndex == IBUS_PACKET_SIZE) {
                  // Full packet received
                  packetAvailable = true;
                  packetIndex = 0;  // Reset for next packet
                }
                break;
            }
          }
        }
    
        // Function to process the received IBUS packet
        bool processIBUSPacket() {
          if (!packetAvailable) {
            return false;
          }
    
          // Verify checksum before processing
          if (!verifyChecksum()) {
            packetAvailable = false;
            return false;  // Checksum failed, discard packet
          }
    
          // Extract channel data
          camX    = ibusPacket[2 + 0 * 2] | (ibusPacket[3 + 0 * 2] << 8);
          camY    = ibusPacket[2 + 1 * 2] | (ibusPacket[3 + 1 * 2] << 8);
          moveY   = ibusPacket[2 + 2 * 2] | (ibusPacket[3 + 2 * 2] << 8);
          moveX   = ibusPacket[2 + 3 * 2] | (ibusPacket[3 + 3 * 2] << 8);
          switchA = ibusPacket[2 + 4 * 2] | (ibusPacket[3 + 4 * 2] << 8);
          switchB = ibusPacket[2 + 5 * 2] | (ibusPacket[3 + 5 * 2] << 8);
          switchC = ibusPacket[2 + 6 * 2] | (ibusPacket[3 + 6 * 2] << 8);
          switchD = ibusPacket[2 + 7 * 2] | (ibusPacket[3 + 7 * 2] << 8);
          varA    = ibusPacket[2 + 8 * 2] | (ibusPacket[3 + 8 * 2] << 8);
          varB    = ibusPacket[2 + 9 * 2] | (ibusPacket[3 + 9 * 2] << 8);
    
          packetAvailable = false;
          return true;
        }
    
        // Getters for channel values
        int getCamX()    { return camX; }
        int getCamY()    { return camY; }
        int getMoveX()   { return moveX; }
        int getMoveY()   { return moveY; }
        int getSwitchA() { return switchA; }
        int getSwitchB() { return switchB; }
        int getSwitchC() { return switchC; }
        int getSwitchD() { return switchD; }
        int getVarA()    { return varA; }
        int getVarB()    { return varB; }
    
      private:
        HardwareSerial& serial;
        uint8_t rxPin;
    
        static const int IBUS_PACKET_SIZE = 32;
        static const uint8_t HEADER1 = 0x20;
        static const uint8_t HEADER2 = 0x40;
    
        uint8_t ibusPacket[IBUS_PACKET_SIZE];
        int packetIndex;
        bool packetAvailable;
    
        int camX, camY, moveX, moveY, switchA, switchB, switchC, switchD, varA, varB;
    
        // Function to verify the checksum of the IBUS packet
        bool verifyChecksum() {
          uint16_t checksum = 0xFFFF;
          for (int i = 0; i < IBUS_PACKET_SIZE - 2; i++) {
            checksum -= ibusPacket[i];
          }
          uint16_t receivedChecksum = ibusPacket[IBUS_PACKET_SIZE - 2] | (ibusPacket[IBUS_PACKET_SIZE - 1] << 8);
          return checksum == receivedChecksum;
        }
    };
    
    // --- MyMotor Class ---
    class MyMotor {
      public:
        // Constructor: Initializes MCPWM and sets up pins
        MyMotor(int forwardPin, int reversePin) {
          this->forwardPin = forwardPin;
          this->reversePin = reversePin;
    
          // Initialize MCPWM for forward and reverse pins
          mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0A, forwardPin); // Initialize forward pin for PWM
          mcpwm_gpio_init(MCPWM_UNIT_0, MCPWM0B, reversePin); // Initialize reverse pin for PWM
    
          // Configure MCPWM unit 0, timer 0 for both forward and reverse control
          mcpwm_config_t pwm_config;
          pwm_config.frequency = 1000;     // Set frequency to 1 kHz
          pwm_config.cmpr_a = 0.0;         // Initial duty cycle of PWMxA = 0%
          pwm_config.cmpr_b = 0.0;         // Initial duty cycle of PWMxB = 0%
          pwm_config.counter_mode = MCPWM_UP_COUNTER; // Up counting mode
          pwm_config.duty_mode = MCPWM_DUTY_MODE_0;   // Active high duty
    
          // Initialize MCPWM with the above configuration
          mcpwm_init(MCPWM_UNIT_0, MCPWM_TIMER_0, &pwm_config);
        }
    
        // Method to set speed and direction: -100 to 100
        void setSpeed(int speed) {
          if (speed < -100) speed = -100;   // Limit speed to minimum -100
          if (speed > 100) speed = 100;     // Limit speed to maximum 100
    
          // Calculate the absolute duty cycle percentage (0 to 100)
          float dutyCycle = abs(speed);
    
          // Forward direction
          if (speed > 0) {
            mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, dutyCycle); // Set duty for forward
            mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_B, 0.0);       // Ensure reverse is 0
            //Serial.print("Forward Speed: ");
          }
          // Reverse direction
          else if (speed < 0) {
            mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, 0.0);       // Ensure forward is 0
            mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_B, dutyCycle); // Set duty for reverse
            //Serial.print("Reverse Speed: ");
          }
          // Stop
          else {
            mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_A, 0.0);       // Stop forward
            mcpwm_set_duty(MCPWM_UNIT_0, MCPWM_TIMER_0, MCPWM_OPR_B, 0.0);       // Stop reverse
            //Serial.println("Motor Stopped");
            return;
          }
    
          // Print the set speed to Serial Monitor
          Serial.print(speed);
          //Serial.println("%");
        }
    
      private:
        int forwardPin;   // GPIO for forward PWM control
        int reversePin;   // GPIO for reverse PWM control
    };
    
    // --- MyServo Class ---
    class MyServo {
      public:
        // Constructor to initialize the servo on a given MCPWM unit, timer, operator, and GPIO pin
        MyServo(mcpwm_unit_t unit, mcpwm_timer_t timer, mcpwm_operator_t op, int gpio_pin) {
          mcpwm_num = unit;
          timer_num = timer;
          op_num = op;
          gpio_num = gpio_pin;
    
          // Initialize MCPWM GPIO pin
          mcpwm_gpio_init(mcpwm_num, getSignal(), gpio_num);
    
          // Configure MCPWM unit
          mcpwm_config_t pwm_config;
          pwm_config.frequency = 50;  // Frequency = 50Hz for servo
          pwm_config.cmpr_a = 0;      // Duty cycle for PWMxA = 0
          pwm_config.cmpr_b = 0;      // Duty cycle for PWMxB = 0
          pwm_config.counter_mode = MCPWM_UP_COUNTER;
          pwm_config.duty_mode = MCPWM_DUTY_MODE_0;
    
          // Initialize MCPWM with the configuration
          mcpwm_init(mcpwm_num, timer_num, &pwm_config);
        }
    
        // Method to set the servo angle (75 to 195 degrees)
        void setAngle(int angle) {
          float duty_cycle = calculateDutyCycle(angle);
          mcpwm_set_duty(mcpwm_num, timer_num, op_num, duty_cycle);
          mcpwm_set_duty_type(mcpwm_num, timer_num, op_num, MCPWM_DUTY_MODE_0);
        }
    
      private:
        mcpwm_unit_t mcpwm_num;       // MCPWM unit number
        mcpwm_timer_t timer_num;      // MCPWM timer number
        mcpwm_operator_t op_num;      // MCPWM operator (A or B)
        int gpio_num;                 // GPIO pin number
    
        // Function to calculate duty cycle based on the desired angle
        float calculateDutyCycle(int angle) {
          // Pulse width range from 0.5ms (0 degrees) to 2.5ms (270 degrees)
          float min_pulse_width = 0.5;  // Minimum pulse width in milliseconds
          float max_pulse_width = 2.5;  // Maximum pulse width in milliseconds
          float frequency = 50.0;       // Frequency in Hz
          float period = 1000.0 / frequency;  // Period in milliseconds (20ms)
    
          // Ensure the angle is within the specified range
          if (angle < 75) angle = 75;
          if (angle > 195) angle = 195;
    
          // Calculate pulse width for the given angle
          float pulse_width = min_pulse_width + ((float)angle / 270.0) * (max_pulse_width - min_pulse_width);
    
          // Calculate duty cycle percentage
          float duty_cycle = (pulse_width / period) * 100.0;
    
          return duty_cycle;
        }
    
        // Helper function to get the correct MCPWM signal for GPIO initialization
        mcpwm_io_signals_t getSignal() {
          if (op_num == MCPWM_OPR_A) {
            if (timer_num == MCPWM_TIMER_0) return MCPWM0A;
            else if (timer_num == MCPWM_TIMER_1) return MCPWM1A;
            else return MCPWM2A;  // MCPWM_TIMER_2
          } else {
            if (timer_num == MCPWM_TIMER_0) return MCPWM0B;
            else if (timer_num == MCPWM_TIMER_1) return MCPWM1B;
            else return MCPWM2B;  // MCPWM_TIMER_2
          }
        }
    };
    
    // --- Global Instances ---
    
    // Create an instance of IBusReceiver
    // For ESP32, Serial2 is typically used for UART communication
    IBusReceiver ibus(Serial2, 16);  // Assuming RX is on GPIO 16
    
    // Create an instance of the MyMotor class
    MyMotor motor(4, 15);  // Use GPIO 4 for forward, GPIO 15 for reverse
    
    // Create an instance of the MyServo class
    MyServo servo_wheel(MCPWM_UNIT_1, MCPWM_TIMER_0, MCPWM_OPR_A, 25);  // Servo on GPIO25
    
    void setup() {
      Serial.begin(115200);  // Initialize Serial Monitor
      ibus.begin(115200);    // Initialize IBUS receiver with baud rate 115200
    
      Serial.println("Starting Motor and Servo Control with iBUS Receiver...");
    
      // Optional: Give some time for serial ports to initialize
      delay(1000);
    }
    
    void loop() {
      // Read IBUS data
      ibus.readIBUSData();
    
      // Process IBUS packet if available
      if (ibus.processIBUSPacket()) {
        // Retrieve the moveY and moveX channel values
        int moveY = ibus.getMoveY();
        int moveX = ibus.getMoveX();
        int camX = ibus.getCamX();
        int camY = ibus.getCamY();
    
        // Map moveY from 1000-2000 to -100 to 100
        int motorSpeed = map(moveY, 1000, 2000, -100, 100);
        motor.setSpeed(motorSpeed);
    
        // Map moveX from 1000-2000 to 75-195 degrees
        int servo_wheel_angle = map(moveX, 1000, 2000, 80, 190);
        servo_wheel.setAngle(servo_wheel_angle);
    
        // Map camX from 1000-2000 to whatever
        int servo_pan_angle = map(camX, 1000, 2000, 45, 225);
        servo_wheel.setAngle(servo_pan_angle);
        int servo_tilt_angle = map(camX, 1000, 2000, 45, 225);
        servo_wheel.setAngle(servo_tilt_angle);
    
        Serial.print(motorSpeed);
        Serial.print(" ");
        Serial.print(servo_wheel_angle);
        Serial.print(" ");
        Serial.print(servo_pan_angle);
        Serial.print(" ");
        Serial.println(servo_tilt_angle);
      }
    }
    

    Now that we have both the motor drivers and the steering servo controlled by the Esp32,  we can do things like this:

View all 6 instructions

Enjoy this project?

Share

Discussions

Gigawatts wrote 11/12/2024 at 01:05 point

I've considered something similar, I started by replacing the motor controller with one from Pololu that can take several kinds of inputs including R/C PWM. I never made up my mind on a steering mechanism. Could you elaborate at all on how that's mounted and manufactured?

  Are you sure? yes | no

gearscodeandfire wrote 11/12/2024 at 15:38 point

Hi! That was my same first step as well, I used two BTS7960 rated at 43A each, but the total current draw never tripped a 40 amp breaker I used.

I think I put some pictures in for the steering mechanism… there's definitely footage of it in my full video.

I decided to weld a 25T servo attachment to a 20 tooth GT2 timing pulley and a 35kg servo. I then put (I think) a 60 tooth pull onto the steering shaft. The steering shaft is 9 mm, so I had to order a 9 mm drillbit to make the bore fit the shaft.

then I really clutched together some welded aluminum to give the mechanism rigidity, and it wound up working pretty well. If I were to do it again, I would use more of a gear reduction between the police, or a stronger servo, or both.

Hope this is helpful, feel free to reach out if you have any more questions!

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates