Freenove / Freenove_4WD_Smart_Car_Kit_for_Raspberry_Pi

Apply to FNK0043
Other
147 stars 159 forks source link

Add support for Donkeycar #41

Open Ezward opened 1 year ago

Ezward commented 1 year ago

Donkeycar

I am one of the maintainers of the Donkeycar project. We have recently had a user ask about running the Donkeycar code on his Freenove 4WD Smart Car Kit for Raspberry Pi. I believe it could easily be done. Perhaps you might do this work and make Donkeycar available to your users. Here is what I suggested to him; if Freenove could do this work and test it, then you could open a pull request into donkeycar and add value for your users.

Looking at the github, it appears the Freenove 4WD Smart Car Kit for Raspberry Pi is using a PCA9685 to generate the duty cycle for 4 motors; https://github.com/Freenove/Freenove_4WD_Smart_Car_Kit_for_Raspberry_Pi/blob/master/Code/Server/Motor.py. The 4 motors, one per wheel, each use two PWM outputs from the PCA9685; one for forward duty cycle and one for reverse duty cycle.

The closest drivetrain the Donkeycar project has is DRIVE_TRAIN_TYPE == "DC_TWO_WHEEL", which uses two PWM outputs each for two motors (a left and right motor) as in a typicaly differential drive configuration. So you need to 1) create a new drivetrain type in manage.py/complete.py, 2) Add configuration for the new drivetrain to myconfig.py/cfg_complete.py and make sure it is uncommented. 3) set DRIVE_TRAIN_TYPE == <new_configuration_name> in myconfig.py. I'll detail these steps below:

1) So you could create a new drivetrain from the code at https://github.com/autorope/donkeycar/blob/5d7fd5014e3eb0831a270b57f5b8f7783787fc47/donkeycar/templates/complete.py#L903 that creates actuator.L298N_HBridge_2pin part for each of the 4 motors. Something like this;

        elif cfg.DRIVE_TRAIN_TYPE == "DC_TWO_WHEEL_SKID":
            dt = cfg.DC_TWO_WHEEL_SKID
            left_front_motor = actuator.L298N_HBridge_2pin(
                pins.pwm_pin_by_id(dt['LEFT_FRONT_FWD_DUTY_PIN']),
                pins.pwm_pin_by_id(dt['LEFT_FRONT_BWD_DUTY_PIN']))
            left_rear_motor = actuator.L298N_HBridge_2pin(
                pins.pwm_pin_by_id(dt['LEFT_REAR_FWD_DUTY_PIN']),
                pins.pwm_pin_by_id(dt['LEFT_REAR_BWD_DUTY_PIN']))
            right_front_motor = actuator.L298N_HBridge_2pin(
                pins.pwm_pin_by_id(dt['RIGHT_FRONT_FWD_DUTY_PIN']),
                pins.pwm_pin_by_id(dt['RIGHT_FRONT_BWD_DUTY_PIN']))
            right_rear_motor = actuator.L298N_HBridge_2pin(
                pins.pwm_pin_by_id(dt['RIGHT_REAR_FWD_DUTY_PIN']),
                pins.pwm_pin_by_id(dt['RIGHT_REAR_BWD_DUTY_PIN']))

            V.add(left_front_motor, inputs=['left/throttle'])
            V.add(left_rear_motor, inputs=['left/throttle'])
            V.add(right_front_motor, inputs=['right/throttle'])
            V.add(right_rear_motor, inputs=['right/throttle'])

2) Modify your myconfig.py file to support DRIVE_TRAIN_TYPE == "DC_TWO_WHEEL_SKID" by copying the DC_TWO_WHEEL configuration block at https://github.com/autorope/donkeycar/blob/5d7fd5014e3eb0831a270b57f5b8f7783787fc47/donkeycar/templates/cfg_complete.py#L290, like;

DC_TWO_WHEEL_SKID = {
    "LEFT_FRONT_FWD_DUTY_PIN": "PCA9685.1:40.3",  # pwm pin produces duty cycle for left front wheel forward
    "LEFT_FRONT_BWD_DUTY_PIN": "PCA9685.1:40.2",  # pwm pin produces duty cycle for left front wheel reverse
    "LEFT_REAR_FWD_DUTY_PIN": "PCA9685.1:40.1",  # pwm pin produces duty cycle for left rear wheel forward
    "LEFT_REAR_BWD_DUTY_PIN": "PCA9685.1:40.0",  # pwm pin produces duty cycle for left rear wheel reverse
    "RIGHT_FRONT_FWD_DUTY_PIN": "PCA9685.1:40.4", # pwm pin produces duty cycle for right front wheel forward
    "RIGHT_FRONT_BWD_DUTY_PIN": "PCA9685.1:40.5", # pwm pin produces duty cycle for right front wheel reverse
    "RIGHT_REAR_FWD_DUTY_PIN": "PCA9685.1:40.6", # pwm pin produces duty cycle for right rear wheel forward
    "RIGHT_REAR_BWD_DUTY_PIN": "PCA9685.1:40.7", # pwm pin produces duty cycle for right rear wheel reverse
}

3) set DRIVE_TRAIN_TYPE == "DC_TWO_WHEEL_SKID" in your myconfig.py

NOTE myconfig.py is really just a copy of cfg_complete.py and manage.py is really just a copy of complete.py (both linked above). So if you want to make integrate the changes into the donkeycar framework, then you could change those files. The when you create a new mycar application using the donkey createcar path=~/mycar command it will create a myconfig.py and manage.py with the changes. If you do that and your testing shows that car works, please open a pull request and we can make that new drivetrain part of the regular donkeycar code.

jjakubassa commented 1 year ago

I'm very interested in the autonomous driving features by the Donkeycar project. It would be great if the freenove 4wd smart car would support this.

Ezward commented 1 year ago

The code I added above will handle the drive train. We must also consider the camera. The camera is a standard Raspberry Pi camera, so the default CAMERA_TYPE = "PICAM" will work. The camera is mounted on a two axis, servo controlled mount. So we will need some code to initialize the camera position. It appears to be using channels 8 to 15 on the PCA9685 to control servos, https://github.com/Freenove/Freenove_4WD_Smart_Car_Kit_for_Raspberry_Pi/blob/faa05beb1a29668aacaf331609e4ce62b01d9353/Code/Server/servo.py#L7 I am guessing channel 8 and 9 are for the camera mount, since they are explicitly set to center position at startup.

In donkey we can use the PulseController https://github.com/autorope/donkeycar/blob/5d7fd5014e3eb0831a270b57f5b8f7783787fc47/donkeycar/parts/actuator.py#L85 with an appropriate pin specifier, like PCA9685.1:40.8 for channel 8. We would construct two instance of the PulseController, one for channel 8 and one for channel 9 and add them to the vehicle. Then use the duty_cycle() helper function to calculate the duty cycle for a given pulse length and cycle frequency https://github.com/autorope/donkeycar/blob/5d7fd5014e3eb0831a270b57f5b8f7783787fc47/donkeycar/parts/actuator.py#L55, like duty_cycle(1500, 50), and pass the result to the PulseController.set_pulse() method.

We may want to point the camera slightly down so it can see immediately in front of the car.

jjakubassa commented 1 year ago

Regarding the camera position: I copied https://github.com/Freenove/Freenove_4WD_Smart_Car_Kit_for_Raspberry_Pi/blob/faa05beb1a29668aacaf331609e4ce62b01d9353/Code/Server/servo.py and https://github.com/Freenove/Freenove_4WD_Smart_Car_Kit_for_Raspberry_Pi/blob/faa05beb1a29668aacaf331609e4ce62b01d9353/Code/Server/PCA9685.py into the mycar folder and used the servo class to initialize the camera position once in the drive() function in manage.py.

  import time
  from servo_freenove import Servo

  pwm=Servo()
  pwm.setServoPwm('0',90, 10)
  time.sleep(0.2)
  pwm.setServoPwm('1', 90, 10)
  time.sleep(0.2)