This repository contains the Arduino Code part of the project which controls the Car component.
A small custom-built wooden car, with the following features:
- 10cm width x 20cm length
- Wheels are powered by DC-motors, 4 wheels and 4 motors
- 2 H-bridges, to control the motors (one per two motors)
- Uses differential steering (tank-style turning) where left and right wheels rotate in opposite directions to turn
- An ultrasonic sensor mounted on the front, for obstacle detection
- Arduino Mega, to control the car
- 3 Lithium-batteries, each is 3.7V for a total of 11.1V to power the motors
- Voltage regulator to step down the voltage to 5V for the Arduino
- Small servo to control the rotation of a Pen, which is used to draw as the car moves
- ESP32, to allow wifi communication with the Arduino (e.g. via api endpoints that the Raspberry Pi will call)
- MPU-6050 Gyroscope and Accelerometer, for precise movement tracking and orientation sensing, especially for accurate turning angles
The Arduino Code is written in C++ and is located in the main folder.
The code supports various features exposed via API endpoints.
Any calculation which for example depends on the wheel size or other parameters is done in the code internally, the caller should not have to worry about it.
The code is organized into a class-based architecture to improve maintainability and separation of concerns:
arduino/main/
├── main.ino - Main Arduino sketch file with setup() and loop()
├── include/ - Header files directory
│ ├── Motor.h - Motor class declaration
│ ├── MotionController.h - Motion controller class declaration
│ ├── GyroSensor.h - Gyroscope sensor class declaration
│ ├── PenController.h - Pen controller class declaration
│ ├── UltrasonicSensor.h - Ultrasonic sensor class declaration
│ ├── API.h - API interface class declaration
│ ├── Result.h - Result structure declaration
│ └── MovementParams.h - Movement parameters structure
├── Motor.cpp - Motor class implementation
├── MotionController.cpp - Base motion controller implementation (constructor, setup, basic motor control)
├── MotionController_translate.cpp - Translation movement implementation
├── MotionController_rotate.cpp - Rotation movement implementation
├── GyroSensor.cpp - Gyroscope sensor implementation
├── PenController.cpp - Pen controller implementation
├── UltrasonicSensor.cpp - Ultrasonic sensor implementation
├── API.cpp - API interface implementation
├── Result.cpp - Result structure implementation
└── MovementParams.cpp - Movement parameters implementation
- Motor: Handles individual motor control with direct hardware interaction
- MotionController: Manages all movement operations using the motors
- Split into three files:
- MotionController.cpp: Core functionality and motor control
- MotionController_translate.cpp: Linear motion with sophisticated yaw correction
- MotionController_rotate.cpp: PID-based rotation control with direction change tracking
- Split into three files:
- GyroSensor: Handles gyroscope readings and orientation calculations
- PenController: Controls the pen servo operations
- UltrasonicSensor: Manages distance sensing
- API: Processes commands and interfaces with all other components
The car's API accepts commands via the serial port in a specific format: command:json_payload. After processing the command, it returns a JSON response.
All commands follow this format:
command:{"param1":"value1","param2":value2}
Where:
commandis the API endpoint (move, rotate, pen, gyro, sensor)- The JSON payload contains the parameters specific to that command
- Moving forward 20cm at speed 100:
move:{"speed":100,"distance":20}
Response:
{"success":true,"result":"Moved at speed 100 for 784ms"}- Moving backward for 1 second at speed 150:
move:{"speed":-150,"timeMs":1000}
Response:
{"success":true,"result":"Moved at speed -150 for 1000ms"}- Failed move due to obstacle detection:
move:{"speed":100,"distance":30}
Response:
{"success":false,"reason":"Obstacle detected at 12.45cm"}- Turning left (counterclockwise) 90 degrees:
rotate:{"angle":90,"speed":100}
Response:
{"success":true,"result":"Rotated 90.00 degrees"}- Turning right (clockwise) 45 degrees:
rotate:{"angle":-45,"speed":80}
Response:
{"success":true,"result":"Rotated -45.00 degrees"}- Rotating to absolute heading (North = 0 degrees):
rotate:{"angle":0,"speed":100,"absolute":true}
Response:
{"success":true,"result":"Rotated -37.50 degrees"}- Lifting the pen up:
pen:{"action":"up"}
Response:
{"success":true,"result":"Pen lifted up"}- Putting the pen down:
pen:{"action":"down"}
Response:
{"success":true,"result":"Pen put down"}- Setting a custom pen position:
pen:{"action":"position","position":45}
Response:
{"success":true,"result":"Pen position set to 45"}- Reading the distance to the nearest object:
sensor:{"action":"distance"}
Response:
{"success":true,"result":"24.37"}- Checking for obstacles within a threshold:
sensor:{"action":"obstacle","threshold":15}
Response:
{"success":true,"result":"false"}- Calibrating the gyroscope:
gyro:{"action":"calibrate"}
Response:
{"success":true,"result":"Gyro calibrated"}- Getting current gyro data:
gyro:{"action":"data"}
Response:
{"success":true,"result":{"accelX":0.012345,"accelY":-0.987654,"accelZ":9.812345,"gyroX":0.000123,"gyroY":0.000456,"gyroZ":0.000789,"temperature":23.45}}- Getting the current yaw angle:
gyro:{"action":"yaw"}
Response:
{"success":true,"result":"127.84"}- Setting the reference orientation:
gyro:{"action":"reference"}
Response:
{"success":true,"result":"Reference yaw set"}All commands to the Arduino follow this format: command:{"param1":"value1","param2":value2}
Responses are always JSON objects with the following structure:
{
"success": true|false,
"failure_reason": "string", // only if success is false
"success_result": "string or json object" // only if success is true
}Command: move
Parameters:
- You must provide at least 2 of these 3 parameters:
speed(int): -255 to 255, negative values for backward movementdistance(float): Distance in centimeterstimeMs(unsigned long): Time in milliseconds
- Optional parameters:
checkUltrasonic(boolean): Whether to check for obstacles (default: true)enableYawCorrection(boolean): Whether to use gyro for straight movement (default: true)
Parameter Combinations:
speed+distance: Car will calculate the required timespeed+timeMs: Car will calculate the distance covereddistance+timeMs: Car will calculate the required speed
Examples:
move:{"speed":100,"distance":20}
move:{"speed":-150,"timeMs":1000}
move:{"distance":30,"timeMs":2000}
move:{"speed":200,"distance":50,"checkUltrasonic":false}
Success Response:
{
"success": true,
"success_result": "{\"distance_traveled\":20.00,\"time_taken\":784,\"final_yaw\":0.32}"
}Failure Response:
{
"success": false,
"failure_reason": "Obstacle detected at 12.45cm"
}Command: rotate
Parameters:
- Required:
angle(float): Angle in degrees. Positive for left/CCW, negative for right/CW
- Optional:
speed(int): Rotation speed between 1-255 (default: 100)absolute(boolean): If true, rotate to absolute heading; if false, relative rotation (default: false)
Examples:
rotate:{"angle":90,"speed":100}
rotate:{"angle":-45,"speed":80}
rotate:{"angle":0,"speed":100,"absolute":true}
Success Response:
{
"success": true,
"success_result": "{\"angle_turned\":90.00,\"time_ms\":856,\"direction_changes\":0}"
}Failure Response:
{
"success": false,
"failure_reason": "Invalid speed. Must be between 1 and 255."
}Command: pen
Parameters:
- Required:
action(string): One of "up", "down", or "position"
- Required for "position" action:
position(int): Position value for the servo (typically 0-180)
Examples:
pen:{"action":"up"}
pen:{"action":"down"}
pen:{"action":"position","position":45}
Success Response:
{
"success": true,
"success_result": "Pen lifted up"
}Failure Response:
{
"success": false,
"failure_reason": "Missing or invalid 'position' parameter for pen position"
}Command: gyro
Parameters:
- Required:
action(string): One of "calibrate", "data", "yaw", or "reference"
Examples:
gyro:{"action":"calibrate"}
gyro:{"action":"data"}
gyro:{"action":"yaw"}
gyro:{"action":"reference"}
Success Response for "calibrate":
{
"success": true,
"success_result": "Gyro calibrated"
}Success Response for "data":
{
"success": true,
"success_result": "{\"accelX\":0.012345,\"accelY\":-0.987654,\"accelZ\":9.812345,\"gyroX\":0.000123,\"gyroY\":0.000456,\"gyroZ\":0.000789,\"temperature\":23.45}"
}Success Response for "yaw":
{
"success": true,
"success_result": "127.84"
}Success Response for "reference":
{
"success": true,
"success_result": "Reference yaw set"
}Failure Response:
{
"success": false,
"failure_reason": "Unknown gyro action: invalid_action"
}Command: sensor
Parameters:
- Required:
action(string): One of "distance" or "obstacle"
- Optional for "obstacle" action:
threshold(float): Distance threshold in centimeters (default: 10.0)
Examples:
sensor:{"action":"distance"}
sensor:{"action":"obstacle","threshold":15}
Success Response for "distance":
{
"success": true,
"success_result": "24.37"
}Success Response for "obstacle":
{
"success": true,
"success_result": "false"
}Failure Response:
{
"success": false,
"failure_reason": "Unknown sensor action: invalid_action"
}- All commands are validated for proper format and parameter values
- If a required parameter is missing or has an invalid value, the command will fail
- In case of JSON parsing errors, you'll receive an error response
- For movement commands, calculations ensure reasonable values
- Obstacle detection during movement will abort the command and return a failure with the obstacle distance
- Commands must end with a newline character ('\n')
- Serial communication uses 115200 baud rate
- Responses will be sent as a complete JSON string followed by a newline
- All JSON values are properly escaped to ensure valid parsing
- Numeric values in responses have appropriate precision
- Float values typically have 2 decimal places
- Integer values are represented without decimal places