To build an Arduino or ESP32-powered 4-wheel robot car, you need some specific parts including a special chassis. In the case of ordinary 4-wheel drive, the wheel mechanism is less complex. You need rear-wheel drive plus a front steering mechanism to properly steer. As this kind of chassis is built using the professional remote-control racing principle. They use two rods through the steering servo to control the direction of the car. The metal fixing piece is tightly engaged with the drive shaft.
Because they use more delicate metal parts including bearings, transmission gear, connecting rod, metal motor, steering cup, and multi-function bracket, they become expensive. Also, it is prudent to purchase the kit with DC motors and servo (since they are correctly matched). The suitable motor in this case is JGA25-370 and a S3003 Metal Servo will work fine. The DC motor will get installed as in the below photograph:

Probably you will have to purchase the unit from China. Here are some kits selling:
---
1 2 3 4 5 | https://www.aliexpress.com/item/32787752135.html?spm=a2g0o.detail.0.0.608595Uf95UfwZ https://imall.com/product/4-Wheel-DIY-Servo-Robot-Car-4WD-Chassis-Smart-for-Arduino-Platform-with-Metal-Bearing-Kit-Steering-Gear-Control-RPI/Electronic-Components-Supplies-Active/aliexpress.com/1005004001907154/144-174079146/en https://www.elecrow.com/4wd-smart-car-robot-chassis-for-arduino-servo-steering.html |
The above kit will cost you around $75. Be very careful around assembling and installation of the servo motor. Make sure that the servo motor can rotate. You can use PM-R3 as a Motor driver board. You need a 7.4V 18650 battery and some jumper wire. You can control the total thing with Bluetooth via a smartphone app. The wiring will be like the below photograph:

The photos are from this (Chinglish) guide – https://www.instructables.com/Servo-Motor-Car-With-Arduino/
As because they have used a PS2 wireless controller, you can see the black PS2 controller module connected (lower middle part of the photo). Using a PS2 gamepad will not help you if you want to control the internet or add a camera.
For Arduino UNO, you need a Bluetooth module. Here is an example sketch for that purpose:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | /*** android app 连接 bluetooth 4.0 ble 实现远程通信 根据数据指令判断 - 默认小车控制指令:0X70/112,实现局域网遥控功能 通信数据格式:[$M>](数据头)+[data length] +[code] +[data] +[checksum] 例1: 24 4d 3e 00 72 72 24 数据头1 $ 4d 数据头2 M 3e 数据头3 > 00 data length 无数据 72 code 指令 72 checksum 检验和 除数据头外的数据异或得到 例2: 24 4d 3e 02 72 64 64 70 24 数据头1 $ 4d 数据头2 M 3e 数据头3 > 02 data length 数据字节数 72 code 指令 64 data 1 64 data 2 70 checksum 检验和 连接:(可根据实际情况更改) by yfrobot -- [url=http://www.yfrobot.com]www.yfrobot.com[/url] 24/5/2016 */ #include <Servo.h> #include <SoftwareSerial.h> #define DEBUG 1 SoftwareSerial mySerial(12, 11); // RX, TX Servo myservo; #define C_Joystick 112 // Joystick code #define PWM 5 // left motor #define DIR 4 // left motorZ #define SERVOPIN 3 //servo's pin #define SERVOMID 90 #define INBUF_SIZE 128 static uint8_t inBuf[INBUF_SIZE]; static uint8_t dataLenght; static uint8_t checksum; static uint8_t indRX; static uint8_t cmdMSP; uint8_t read8() { return inBuf[indRX++] & 0xff; } uint8_t readThorttle = 128; uint8_t readDirection = 128; void setup() { // put your setup code here, to run once: Serial.begin(115200); mySerial.begin(115200); myservo.attach(SERVOPIN); SERVOMID; delay(10); pinMode(PWM, OUTPUT); pinMode(DIR, OUTPUT); } void loop() { // put your main code here, to run repeatedly: BleReceive(); setMotor(readThorttle, readDirection); } /** evaluate command -- 数据处理 */ void evaluateCommand() { switch (cmdMSP) { case C_Joystick: readThorttle = read8(); readDirection = read8(); // if (DEBUG) { // mySerial.print("th - "); // mySerial.print(readThorttle); // mySerial.print("di - "); // mySerial.println(readDirection); // } break; } } void setMotor(int speed , int dir) //电机驱动函数 { if (speed < 128) { digitalWrite(DIR, LOW); analogWrite(PWM, map(speed, 0, 127, 255, 0)); //mySerial.println("forward"); }else if (speed > 128 ) { digitalWrite(DIR, HIGH); analogWrite(PWM, map(speed, 129, 255, 0, 255)); //mySerial.println("back"); }else { digitalWrite(DIR, LOW); analogWrite(PWM, LOW); } if (dir < 128){ myservo.write(map(dir,0,127,50,89)); }else if (dir > 128){ myservo.write(map(dir,129,255,91,120)); }else { myservo.write(SERVOMID); } delay(50); } /** receive data function */ void BleReceive() { uint8_t c; static uint8_t dire_offset; static uint8_t dataSize; static enum _serial_state { IDLE, HEADER_START, HEADER_M, HEADER_ARROW, HEADER_SIZE, HEADER_CMD, } c_state = IDLE; while (Serial.available() > 0) { c = Serial.read(); // 读串口缓冲区 if (c_state == IDLE) { //串口状态空闲 等待HEADER_START状态的到来 c_state = (c == '$') ? HEADER_START : IDLE; //判定是$字符吗?是则进入HEADER_START状态 if (c_state == IDLE) {}// evaluateOtherData(c); // evaluate all other incoming serial data } else if (c_state == HEADER_START) { c_state = (c == 'M') ? HEADER_M : IDLE; //判定是M字符吗?是则进入HEADER_M状态 } else if (c_state == HEADER_M) { c_state = (c == '>') ? HEADER_ARROW : IDLE; //判定是>字符吗?是则进入HEADER_ARROW状态 } else if (c_state == HEADER_ARROW) { //是ARROW字符,进入HEADER_ARROW状态,判定缓冲区的大小 if (c > INBUF_SIZE) { // now we are expecting the payload size 我们期望足够的数据占用缓冲区 c_state = IDLE; //数据位置不够 退出循环 continue; //不执行该while循环包含的后面的语句,跳出开始下一轮循环 } dataSize = c; dataLenght = dataSize; dire_offset = 0; checksum = 0; indRX = 0; checksum ^= c; //校验和 1 - dataSize c_state = HEADER_SIZE; // the command is to follow 接收到数据长度,进入HEADER_SIZE状态 } else if (c_state == HEADER_SIZE) { cmdMSP = c; //接收 指令(code) checksum ^= c; //校验和 2 - code c_state = HEADER_CMD; //接收到指令,进入HEADER_CMD状态 } else if (c_state == HEADER_CMD && dire_offset < dataSize) { checksum ^= c; //校验和 3 - data inBuf[dire_offset++] = c; } else if (c_state == HEADER_CMD && dire_offset >= dataSize) { if (checksum == c) {// compare calculated and transferred checksum evaluateCommand(); //数据处理程序 } c_state = IDLE; //返回IDLE状态 } } } |