EC6090 Robotics & Automation Mini Project β University of Jaffna
An autonomous line-following mobile robot with color-based obstacle avoidance and pick-and-place. Supports two line-following modes:
- PID Mode (default) β traditional PID controller, runs directly on ESP32
- RL Mode (optional) β Q-Learning trained in simulation, exported to ESP32
Both modes can be tested in Pygame simulation before deploying to hardware.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PC SIMULATION β
β β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββ β
β β PID Test β β RL Training β β
β β (pid_test.py) β β (train.py β visualize) β β
β β Test ESP32 PID β β Train Q-Learning agent β β
β β logic in Pygame β β then export Q-table β β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β tune PID gains β q_table.h
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ESP32 FIRMWARE β
β ββββββββββββ βββββββββββββββββ ββββββββββββββββ β
β β Sensors ββ β PID or RL ββ β Motors β β
β β IR / β β Line Follower β β Servo β β
β β Color β β + State β β Gripper β β
β β β β Machine β β β β
β ββββββββββββ βββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ββ ββ 10cm IR Array ββ ββ β
Front [S1] [S2] [S3] [S4]
β β β β
-4cm -1.5cm +1.5cm +4cm β distance from centre (firmware .env / robot.py)
β 2.5cm β Color Sensor (gripper base)
β 6.0cm β Gripper Tip (max claw gap: 5.5cm)
β 7.5cm back β Wheel Axis (13cm between wheels)
β 19.0cm back β Caster Wheel
Sensor spacing is configurable β see βοΈ Configuring IR Sensor Distances below.
PID mode works out of the box. No training needed.
cd simulation
pip install -r requirements.txt
# Run PID simulation on the simple linear track (good starting point)
python3 pid_test.py --track linear
# Test on the default loop track
python3 pid_test.py
# Test generalization on different track layouts:
# python3 pid_test.py --track eval_1
# python3 pid_test.py --track eval_2
# python3 pid_test.py --track eval_3Interactive controls in PID simulation:
| Key | Action |
|---|---|
SPACE |
Pause / Resume |
R |
Reset robot to start |
β / β |
Increase / Decrease Kp |
β / β |
Decrease / Increase Kd |
1 - 9 |
Set base speed (1=slow, 9=fast) |
Q / ESC |
Quit |
Use the simulation to find good PID values, then update firmware/.env (see below).
cd firmware
pip install platformio # if not installed
pio run -t upload
pio device monitor- Place robot at START on the arena
- Press the push button
- Robot autonomously follows the line using PID
RL mode requires training in simulation first.
cd simulation
pip install -r requirements.txt
# Train on the simple linear track (fastest convergence β great for first run)
python3 train.py --track linear
# Train on the default loop track
python3 train.py
# Train on all tracks for maximum generalization:
# python3 train.py --track all
# (includes: default, linear, eval_1, eval_2, eval_3)
# Note: The Q-table is saved to q_tables/ with a timestamp
# (e.g. q_table_20260320_230000.npy) β old models are never overwritten.cd simulation
# Automatically loads the MOST RECENTLY trained Q-table:
python3 visualize.py
# Evaluate on specific tracks:
python3 visualize.py --track linear
python3 visualize.py --track eval_1
python3 visualize.py --track eval_2
python3 visualize.py --track eval_3
# Load a SPECIFIC older model:
# python3 visualize.py --model q_table_20260320_230000.npycd simulation
# Exports the MOST RECENTLY trained Q-table to firmware/src/q_table.h
python3 export_qtable.py
# Export a SPECIFIC model:
# python3 export_qtable.py --model q_table_20260320_230000.npyUse firmware/examples/main-RL.cpp as your main.cpp, or add the RL logic to handleLineFollowing():
// REPLACE THIS (PID mode):
bool lineDetected = line_follower_update();
// WITH THIS (RL mode β discrete actions mapped to config.h speeds):
int lineReadings[4];
sensors_read_line(lineReadings);
uint8_t lineState = sensors_get_line_state(lineReadings);
uint16_t stateIndex = lineState * 6 + (uint8_t)color * 2 + (carryingCube ? 1 : 0);
if (stateIndex < Q_NUM_STATES) {
uint8_t action = Q_POLICY[stateIndex];
switch (action) {
case 0: motors_set(SPEED_FULL, SPEED_FULL); break; // FWD
case 1: motors_set(SPEED_SLOW, SPEED_FAST); break; // SL
case 2: motors_set(SPEED_FAST, SPEED_SLOW); break; // SR
case 3: motors_set(-SPEED_TURN, SPEED_TURN); break; // HL
case 4: motors_set(SPEED_TURN, -SPEED_TURN); break; // HR
case 5: motors_stop(); break; // STOP
case 6: motors_set(-SPEED_REVERSE, -SPEED_REVERSE); break; // REV
}
} else {
motors_stop();
}
bool lineDetected = (lineState != 0);Also add #include "q_table.h" at the top of main.cpp. The exported q_table.h contains the trained action policy, and you configure the resulting motor speeds manually in firmware/src/config.h using SPEED_FULL, SPEED_FAST, etc.
cd firmware
pio run -t uploadSensor geometry, PID gains, and motor ranges for the firmware are controlled via a plain-text .env file. No firmware source code editing is required for these settings.
firmware/.env β read by platformio.ini β injected as -D flags β config.h
# βββ IR Sensor Geometry βββββββββββββββββββββββββββββββββββββββ
IR_OUTER_DIST_CM=4.0 # S1/S4 distance from centre (cm)
IR_INNER_DIST_CM=2.0 # S2/S3 distance from centre (cm)
# βββ PID Gains ββββββββββββββββββββββββββββββββββββββββββββββββ
PID_KP=30.0
PID_KI=0.0
PID_KD=20.0
PID_BASE_SPEED=160
# βββ Line Detection βββββββββββββββββββββββββββββββββββββββββββ
LINE_THRESHOLD=2000 # ADC threshold (12-bit, 0β4095)After editing PID/sensor values, rebuild and re-flash:
cd firmware
pio run -t uploadThe PC simulation does not use a .env file. To configure sensor geometries for training or testing, open simulation/robot.py and edit the configuration block at the very top of the file:
IR_OUTER_DIST_CM = 4.0 # S1 (far left) & S4 (far right)
IR_INNER_DIST_CM = 2.0 # S2 (inner left) & S3 (inner right)After modifying this, running train.py or pid_test.py will instantly reflect the changes. Keep these values identical to your firmware's .env for accurate transfers!
| Track | --track flag |
Description |
|---|---|---|
| Linear | linear |
β Simple straight line β fastest convergence, great for first training run |
| Default | default |
Oval + curves β the primary training loop |
| Eval 1 | eval_1 |
S-curve layout |
| Eval 2 | eval_2 |
U-turn layout |
| Eval 3 | eval_3 |
Complex zig-zag loop |
| All | all |
Interleave all 5 tracks for maximum generalization |
Use --track linear first to verify the robot can learn at all, then progress to harder layouts.
| PID Mode | RL Mode | |
|---|---|---|
| Setup time | Instant β flash and go | ~5 min training + export |
| Simulation test | python3 pid_test.py |
python3 train.py then python3 visualize.py |
| Algorithm | PID controller | Q-Learning lookup table |
| Tuning | Adjust Kp, Ki, Kd, speed | Adjust learning rate, episodes, rewards |
| Line following | Smooth, continuous correction | Trained policy table mapping to manual speeds |
| Adaptability | Fixed logic, works on any track | Learns from specific arena layout |
| Code change | None (default) | Modify handleLineFollowing() in main.cpp |
| Best for | Quick deployment, simple tracks | Complex arenas, research projects |
LineBee/
βββ simulation/ # PC simulation environment
β βββ pid_test.py # β
Test PID logic (same as ESP32)
β βββ train.py # β
Train RL agent (supports --track linear/all/...)
β βββ visualize.py # Watch trained RL agent
β βββ export_qtable.py # Export RL policy β C header
β βββ arena.py # Pygame arena with BΓ©zier tracks (5 layouts)
β βββ robot.py # Virtual robot β β
edit IR sensor geometry here
β βββ sensors.py # Simulated IR + color sensors
β βββ rl_agent.py # Q-learning agent (96 states Γ 7 discrete actions)
β βββ requirements.txt # Python dependencies
β βββ q_tables/ # Timestamped trained models (auto-created)
βββ firmware/ # ESP32 code (PlatformIO)
β βββ .env # β
IR sensor distances + PID gains (edit here)
β βββ platformio.ini # Reads .env and injects as build flags
β βββ examples/
β β βββ calibrate.cpp # β
Auto-calibrate per-sensor thresholds
β β βββ ir_test.cpp # Raw IR sensor value test
β β βββ motor_test.cpp # Motor direction/speed test
β β βββ servo_test.cpp # Gripper open/close test
β β βββ color_test.cpp # Color detection test
β β βββ main-pid-linefollower.cpp # Standalone PID example
β β βββ main-normal.cpp # Basic line follower + obstacle avoidance
β β βββ main-RL.cpp # RL mode (Q-table based)
β βββ src/
β βββ main.cpp # State machine (PID default / RL optional)
β βββ config.h # Pins + constants (reads .env values via #ifndef)
β βββ thresholds.h # β
Per-sensor calibrated thresholds (auto-generated)
β βββ motors.cpp/h # L298N motor control
β βββ sensors.cpp/h # 4-way IR line sensors (uses thresholds.h)
β βββ color_sensor.cpp/h # TCS34725 color detection
β βββ gripper.cpp/h # MG-995 servo gripper
β βββ line_follower.cpp/h # PID line following
β βββ obstacle_avoid.cpp/h # Color-based avoidance
β βββ q_table.h # Stub (PID) or trained policy (RL)
βββ docs/
βββ wiring_guide.md # Wiring + robot layout diagram
| Component | Purpose |
|---|---|
| ESP32 NodeMCU | Microcontroller |
| L298N Motor Driver | DC motor control |
| 2Γ DC Gear Motors + Wheels | Drive system (13cm apart) |
| 4-Way IR Line Sensor | Line detection (10cm array, sensor spacing via .env) |
| TCS34725 Color Sensor | Red/green detection (at gripper base) |
| MG-995 Servo | Gripper actuator (5.5cm max gap) |
| Push Button | Start trigger |
See docs/wiring_guide.md for complete wiring and robot layout.
Before running the full autonomous code, it is highly recommended to test each hardware component individually using the provided test scripts in firmware/examples/.
- Install ESP32 Boards (if you haven't already):
- Go to
Arduino β Preferences(Mac) orFile β Preferences(Windows). - In "Additional Boards Manager URLs", paste:
https://dl.espressif.com/dl/package_esp32_index.json - Go to
Tools β Board β Boards Manager, search for "esp32" by Espressif Systems, and click Install.
- Go to
- Go to
Tools β Board β ESP32 Arduinoand select ESP32 Dev Module. - Create a New Sketch (
File β New). - Copy all the contents of the desired test script (e.g.,
firmware/examples/ir_test.cpp) and paste it into your new Arduino sketch. - If testing the servo, go to
Sketch β Include Library β Manage Librariesand install ESP32Servo. - If testing the color sensor, install Adafruit TCS34725 and Adafruit BusIO from the Library Manager.
- Connect your ESP32, select your COM Port (
Tools β Port), and hit Upload. - Open the Serial Monitor and set the baud rate to 115200.
- Open the
LineBee/firmware/folder in VS Code / PlatformIO. - Open
src/main.cpp. Select everything and Delete it. - Open the desired test script (e.g.,
examples/ir_test.cpp), copy all its contents, and paste them intosrc/main.cpp. - Run
pio run -t uploadto flash the ESP32. - Watch the results in the Serial Monitor:
pio device monitor. - When finished testing, remember to restore the original
main.cpp(e.g., viagit restore src/main.cpp).
The calibration sketch measures each sensor's response on the line and floor, then computes per-sensor thresholds automatically.
Steps:
- Copy
examples/calibrate.cppβsrc/main.cpp - Upload:
pio run -t upload - Open Serial Monitor at 115200 baud
- Place all sensors on the LINE β press BUTTON (samples for 5 seconds)
- Move all sensors to the FLOOR β press BUTTON (samples for 5 seconds)
- Copy the generated
thresholds.houtput from the Serial Monitor - Paste it into
firmware/src/thresholds.h(replace entire file) - Restore your original
src/main.cpp(e.g.,git restore src/main.cpp) - Rebuild:
pio run -t upload
The calibration output:
- Shows real-time sensor readings while you position the robot
- Displays a progress bar during sampling
- Rates each sensor's quality (β Excellent / β Good / β OK / β Poor)
- Provides a live verification mode after calibration
- Supports re-calibration by pressing the button again
Why per-sensor thresholds? Each sensor may have slightly different sensitivity due to manufacturing variation, mounting angle, or distance from the surface. Per-sensor calibration ensures all 4 sensors detect the line accurately.
- Goal: Verify that all sensors correctly detect line vs floor using calibrated thresholds.
- Process: Open Serial Monitor. Output shows
S1:xxxx[L] S2:xxxx[_] ...whereL= on line,_= off line. - Note: If readings seem wrong, re-run the calibration sketch.
- Goal: Verify gripper opens wide enough and closes securely.
- Process: The code loops, opening and closing every 2 seconds. Keep hands clear!
- Fix: If the gap is too small or too tight, edit
GRIPPER_OPENandGRIPPER_CLOSEinfirmware/src/config.h.
- Goal: Reliable Red and Green cube detection.
- Process: Place Red and Green cubes directly under the color sensor (~1-2cm distance). Monitor the Serial output for
Detected: REDorDetected: GREEN. - Fix: If it says
Unknown, adjust theRED_*andGREEN_*thresholds inconfig.h.
- Goal: Ensure both wheels spin to move the robot forward.
- Process: Elevate the robot so the wheels can spin freely. The test script runs both motors FORWARD, stops, then REVERSE.
- Fix: If one wheel spins backwards, swap its two wires on the L298N.
For firmware, edit firmware/.env. For simulation, edit simulation/robot.py:
# Outer sensors (S1 far-left, S4 far-right) distance from centre
IR_OUTER_DIST_CM=4.0
# Inner sensors (S2 inner-left, S3 inner-right) distance from centre
IR_INNER_DIST_CM=2.0Rebuild firmware after editing: pio run -t upload
Tune in simulation first (python3 pid_test.py --track linear), then update firmware/.env:
PID_KP=30.0 # Proportional β β/β keys in simulation
PID_KI=0.0 # Integral β start at 0
PID_KD=20.0 # Derivative β β/β keys in simulation
PID_BASE_SPEED=160 # Base motor speed (0-255)Quick tuning process:
- Set
PID_KI=0,PID_KD=0. IncreasePID_KPuntil robot follows line but oscillates - Increase
PID_KDuntil oscillation stops - If robot drifts on straights, add small
PID_KI(e.g.0.5) - Adjust
PID_BASE_SPEEDfor desired speed
Adjust in simulation/train.py:
NUM_EPISODES = 10000 # More = better policy (but slower)
LEARNING_RATE = 0.5 # How fast Q-values update
DISCOUNT_FACTOR = 0.95 # Future reward importance
EPSILON_DECAY = 0.9985 # Exploration decay rateThe Q-learning agent outputs discrete actions (0-6). You can manually tune how fast the robot moves for each action by editing the presets in firmware/src/config.h:
#define SPEED_FULL 200 // Action 0: Forward (both wheels)
#define SPEED_FAST 180 // Actions 1,2: Slight turn (faster wheel)
#define SPEED_SLOW 100 // Actions 1,2: Slight turn (slower wheel)
#define SPEED_TURN 160 // Actions 3,4: Hard pivot turns
#define SPEED_REVERSE 150 // Action 6: Reverse
#define SPEED_MEDIUM 140 // Line-loss recoveryTips:
- If the robot turns too sharply during line following, decrease
SPEED_SLOWand increaseSPEED_FAST. - If your motors stall below a certain PWM (e.g., 40), ensure all speed values are high enough.
Recommended: Auto-calibration β see Sensor Calibration above.
The calibration sketch generates per-sensor thresholds in firmware/src/thresholds.h:
// Auto-generated by calibrate.cpp
#define SENSOR_1_THRESHOLD 1850 // Far Left (GPIO 34)
#define SENSOR_2_THRESHOLD 2100 // Inner Left (GPIO 35)
#define SENSOR_3_THRESHOLD 1920 // Inner Right (GPIO 32)
#define SENSOR_4_THRESHOLD 2050 // Far Right (GPIO 33)Note: The old single
LINE_THRESHOLDin.envis no longer used by the sensor code. All line detection now uses per-sensor thresholds fromthresholds.h.
University of Jaffna β EC6090 Robotics & Automation Course Project.