0% found this document useful (0 votes)
24 views10 pages

Advanced Line-Following Robot Code

The document outlines the design and functionality of an advanced robot with a 5-sensor array for line following, path optimization, and finish detection. It includes features such as PID control, EEPROM storage for paths, and adaptive speed adjustments. The code provided implements various functions for sensor calibration, motor control, and path recording during a dry run and actual operation.

Uploaded by

Mr Dustbin
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
24 views10 pages

Advanced Line-Following Robot Code

The document outlines the design and functionality of an advanced robot with a 5-sensor array for line following, path optimization, and finish detection. It includes features such as PID control, EEPROM storage for paths, and adaptive speed adjustments. The code provided implements various functions for sensor calibration, motor control, and path recording during a dry run and actual operation.

Uploaded by

Mr Dustbin
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd

/*

Advanced Meshmerize Robot Sketch


Features:
- 5-sensor array (auto-calibration)
- PID line following with auto-tune saved to EEPROM
- Dry Run: explore using left-hand rule, record moves, optimize path, save to
EEPROM
- Actual Run: replay optimized path from EEPROM
- Finish detection: large white box detection -> RED LED glow + blinking LED
- Adaptive speed: speed boost on straights, adaptive slowdown on turns
- Safety stop when line lost > 3s
- 16x2 LCD for status, Serial logging for plotting
- Single start button; long-press at power-on clears stored path (force Dry Run)
Notes: tune thresholds, motor timings, and speeds for your bot.
*/

#include <LiquidCrystal.h>
#include <EEPROM.h>

// -------------------- HARDWARE PIN MAP (change as needed) --------------------


LiquidCrystal lcd(2, 3, 4, 5, 6, 13); // RS,EN,D4,D5,D6,D7

// Motor driver pins (L298N-like)


const uint8_t L_PWM = 9; // ENA (PWM)
const uint8_t L_IN1 = 8;
const uint8_t L_IN2 = 7;

const uint8_t R_PWM = 10; // ENB (PWM)


const uint8_t R_IN3 = 12;
const uint8_t R_IN4 = 11;

// Sensors: 5 IR sensors left->right


const uint8_t SENS_PINS[5] = {A0, A1, A2, A3, A4};

// Start button (single switch)


const uint8_t START_BTN = A6; // use any free digital/analog pin (use digital
input_pullup if using digital)

// Red LED that must glow at end zone


const uint8_t RED_LED = A7; // choose a free pin (or digital pin)

// Small status LED (blink)


const uint8_t STATUS_LED = 13; // onboard LED or external

// -------------------- BEHAVIOR TUNING --------------------


// Calibration & thresholds
int minVal[5], maxVal[5];
int threshold = 600; // fallback, will be auto-calculated in calibration

// PID starting values (can be loaded from EEPROM)


float Kp = 20.0, Ki = 0.0, Kd = 30.0;
float errorVal = 0, lastError = 0, integral = 0;
float correction = 0;

// Auto-tune parameters
bool autotuneDone = false;
unsigned long autotuneStart = 0;
const unsigned long AUTOTUNE_TIME = 9000; // ms to run autotune (first run)
// Speed & performance
int baseSpeed = 160; // default cruising speed
int maxSpeed = 230; // max allowed speed on straights (cap PWM <=255)
int turnSpeed = 120; // speed while turning/pivoting
int adaptiveBoost = 40; // boost on straight
float adaptiveFactor = 1.0; // dynamically adjusted

// End detection
int END_WHITE_COUNT = 30; // number of consecutive loops sensing white box to
confirm end
int endWhiteStreak = 0;

// Safety
unsigned long lostStart = 0;
const unsigned long LOST_TIMEOUT = 3000UL; // ms -> stop when line lost for >3s

// Maze path storage in EEPROM


const uint8_t EEPROM_MAGIC = 0xB3;
const int EEPROM_MAGIC_ADDR = 0;
const int EEPROM_LEN_ADDR = 1;
const int EEPROM_PATH_ADDR = 2;
const int PATH_MAX = 200;
char path[PATH_MAX]; int pathLen = 0; // recorded during Dry Run

// Replay buffer for Actual Run


char replay[PATH_MAX]; int replayLen = 0;

// Dry run settings


bool preferLeftHand = true; // left-hand rule for exploration
unsigned long dryRunStart = 0;
const unsigned long DRY_RUN_LIMIT = 180000UL; // 3 minutes in ms
bool dryRunCompleted = false;

// Helper weights for weighted average: scaled for better numeric behavior
const int weights[5] = {-200, -100, 0, 100, 200};

// Misc
bool havePathInEEPROM = false;
bool journeyComplete = false;

// -------------------- UTILS --------------------


void setMotor(int leftPWM, bool leftForward, int rightPWM, bool rightForward) {
// left
digitalWrite(L_IN1, leftForward ? HIGH : LOW);
digitalWrite(L_IN2, leftForward ? LOW : HIGH);
analogWrite(L_PWM, constrain(leftPWM, 0, 255));
// right
digitalWrite(R_IN3, rightForward ? HIGH : LOW);
digitalWrite(R_IN4, rightForward ? LOW : HIGH);
analogWrite(R_PWM, constrain(rightPWM, 0, 255));
}
void stopMotors() {
analogWrite(L_PWM, 0);
analogWrite(R_PWM, 0);
}

// Read raw analog sensor values into array


void readRawSensors(int out[5]) {
for (int i = 0; i < 5; ++i) out[i] = analogRead(SENS_PINS[i]);
}

// Convert raw readings into binary (1 => on line/black, 0 => white) using
threshold
void makeBinaryFromRaw(int raw[5], uint8_t bin[5]) {
for (int i = 0; i < 5; ++i) bin[i] = (raw[i] < threshold) ? 1 : 0; // black=1
}

// Weighted-average position. Returns large sentinel (999) if no sensor sees line.


float computePositionFromBinary(uint8_t bin[5]) {
long sum = 0; long count = 0;
for (int i = 0; i < 5; ++i) {
if (bin[i]) { sum += weights[i]; count++; }
}
if (count == 0) return 999.0f; // no line
return (float)sum / (float)count;
}

// Soft follow: PD (we primarily use PD as Ki is kept low)


void followLinePD(float pos) {
// pos = 999 means lost line; caller should handle lost condition
float err = pos;
float deriv = err - lastError;
integral += err;
correction = (Kp * err) + (Ki * integral) + (Kd * deriv);
lastError = err;

// adaptiveFactor reduces baseSpeed on big errors


float absErr = fabs(err / 200.0f); // normalize to ~0..1
adaptiveFactor = 1.0 - constrain(absErr * 0.7, 0.0, 0.7); // drop speed when big
error

int left = (int)round(baseSpeed * adaptiveFactor - correction);


int right = (int)round(baseSpeed * adaptiveFactor + correction);

// clamp to allowed range


left = constrain(left, 0, maxSpeed);
right = constrain(right, 0, maxSpeed);

setMotor(left, true, right, true);


}

// End box detection: all sensors see white strongly for many loops
bool checkEndBox(int raw[5]) {
bool allWhiteStrong = true;
for (int i = 0; i < 5; ++i) {
// use >= maxVal[i] - margin (or simple threshold)
if (raw[i] < threshold + 120) { allWhiteStrong = false; break; }
}
if (allWhiteStrong) {
endWhiteStreak++;
} else {
if (endWhiteStreak > 0) endWhiteStreak--;
}
return (endWhiteStreak >= END_WHITE_COUNT);
}

// -------------------- CALIBRATION --------------------


void calibrateSensors() {
// initialize extremes
for (int i = 0; i < 5; ++i) { minVal[i] = 1023; maxVal[i] = 0; }

// tiny wiggle to sample black/white under sensor (move robot or manually tilt)
// We'll do a short wiggle: alternate motors to sample environments
unsigned long t0 = millis();
while (millis() - t0 < 1400) { // ~1.4s
setMotor(100, true, 0, false); delay(60); // small left turn
int r[5]; readRawSensors(r); for (int i=0;i<5;i++){ if (r[i] < minVal[i])
minVal[i]=r[i]; if (r[i] > maxVal[i]) maxVal[i]=r[i]; }
setMotor(0, false, 100, true); delay(60); // small right turn
readRawSensors(r); for (int i=0;i<5;i++){ if (r[i] < minVal[i]) minVal[i]=r[i];
if (r[i] > maxVal[i]) maxVal[i]=r[i]; }
}
stopMotors();

// compute average min/max across sensors to set global threshold


long smin = 0, smax = 0;
for (int i = 0; i < 5; ++i) { smin += minVal[i]; smax += maxVal[i]; }
smin /= 5; smax /= 5;
threshold = (int)((smin + smax) / 2);

[Link]();
[Link]("Calib done");
[Link](0,1);
[Link]("Th:");
[Link](threshold);
delay(900);
}

// -------------------- EEPROM: path & PID save/load --------------------


bool loadPathFromEEPROM() {
if ([Link](EEPROM_MAGIC_ADDR) != EEPROM_MAGIC) return false;
int len = [Link](EEPROM_LEN_ADDR);
if (len <= 0 || len >= PATH_MAX) return false;
for (int i = 0; i < len; ++i) path[i] = (char)[Link](EEPROM_PATH_ADDR + i);
pathLen = len;
return true;
}
void savePathToEEPROM() {
[Link](EEPROM_MAGIC_ADDR, EEPROM_MAGIC);
[Link](EEPROM_LEN_ADDR, (uint8_t)pathLen);
for (int i = 0; i < pathLen; ++i) [Link](EEPROM_PATH_ADDR + i,
(uint8_t)path[i]);
}

// PID saving addresses


const int ADDR_PID_FLAG = 100, ADDR_KP = 104, ADDR_KI = 108, ADDR_KD = 112;
void savePIDToEEPROM() {
[Link](ADDR_KP, Kp);
[Link](ADDR_KI, Ki);
[Link](ADDR_KD, Kd);
[Link](ADDR_PID_FLAG, 1);
}
bool loadPIDFromEEPROM() {
if ([Link](ADDR_PID_FLAG) != 1) return false;
[Link](ADDR_KP, Kp);
[Link](ADDR_KI, Ki);
[Link](ADDR_KD, Kd);
return true;
}
void clearPathEEPROM() {
[Link](EEPROM_MAGIC_ADDR, 0x00);
}

// -------------------- PATH RECORD/OPTIMIZE --------------------


// push move char into path buffer
void pushMove(char m) {
if (pathLen < PATH_MAX - 1) { path[pathLen++] = m; path[pathLen] = '\0'; }
}

// simple reduction rules to optimize path (based on earlier rules)


bool reduceOncePath() {
if (pathLen < 3) return false;
char X = path[pathLen - 3], B = path[pathLen - 2], Y = path[pathLen - 1];
if (B != 'B') return false;
char Z = 0;
if (X=='L' && Y=='L') Z='B';
else if (X=='L' && Y=='S') Z='R';
else if (X=='L' && Y=='R') Z='S';
else if (X=='S' && Y=='L') Z='R';
else if (X=='S' && Y=='S') Z='S';
else if (X=='S' && Y=='R') Z='L';
else if (X=='R' && Y=='L') Z='S';
else if (X=='R' && Y=='S') Z='L';
else if (X=='R' && Y=='R') Z='B';
else return false;
path[pathLen - 3] = Z;
pathLen -= 2;
path[pathLen] = '\0';
return true;
}
void optimizePath() { while(reduceOncePath()); }

// -------------------- MOTION PRIMITIVES --------------------


void pivotLeft() {
// pivot left in place until center sensor sees line again
setMotor(turnSpeed, false, turnSpeed, true);
unsigned long t0 = millis();
while (millis()-t0 < 1200) {
int raw[5]; readRawSensors(raw);
uint8_t b[5]; makeBinaryFromRaw(raw,b);
if (b[2]) break;
}
// small forward roll-in
setMotor(turnSpeed, true, turnSpeed, true); delay(80);
}
void pivotRight() {
setMotor(turnSpeed, true, turnSpeed, false);
unsigned long t0 = millis();
while (millis()-t0 < 1200) {
int raw[5]; readRawSensors(raw);
uint8_t b[5]; makeBinaryFromRaw(raw,b);
if (b[2]) break;
}
setMotor(turnSpeed, true, turnSpeed, true); delay(80);
}
void goStraightShort() {
setMotor(turnSpeed+10, true, turnSpeed+10, true);
delay(120);
}

// Choose branch sensing


void senseBranches(bool &left, bool &straight, bool &right, bool &dead) {
int raw[5]; readRawSensors(raw);
uint8_t b[5]; makeBinaryFromRaw(raw,b);
left = (b[0] || b[1]);
right = (b[4] || b[3]);
straight = (b[2] || (b[1] && b[3]));
dead = !(left || straight || right);
}

// -------------------- DRY RUN: explore & learn --------------------


void doDryRun() {
pathLen = 0; dryRunStart = millis(); endWhiteStreak = 0;
[Link](); [Link]("Dry Run...");
while (true) {
// time limit: if exceed DRY_RUN_LIMIT, stop and save what we have
if (millis() - dryRunStart > DRY_RUN_LIMIT) {
[Link](0,1); [Link]("Dry Run timeout");
optimizePath(); savePathToEEPROM();
dryRunCompleted = false;
return;
}
int raw[5]; readRawSensors(raw);
uint8_t b[5]; makeBinaryFromRaw(raw,b);
float pos = computePositionFromBinary(b);

// Check end box


if (checkEndBox(raw)) {
// glow RED LED
digitalWrite(RED_LED, HIGH);
stopMotors();
delay(500);
digitalWrite(RED_LED, LOW);
optimizePath();
savePathToEEPROM();
dryRunCompleted = true;
[Link](); [Link]("Dry Run Done");
delay(600);
return;
}

// sense junctions
bool L,S,R,D;
senseBranches(L,S,R,D);

if (!L && !R && S) {


// straight: follow PD
if (pos != 999.0f) followLinePD(pos);
else { // lost - try small reverse/pivot
setMotor(120, false, 120, false); delay(120);
}
continue;
}

if (D) {
// dead end -> backtrack rotation
pushMove('B');
setMotor(turnSpeed, true, turnSpeed, false); delay(450); // 180-ish
// wait reacquire
unsigned long t0 = millis();
while (millis() - t0 < 1000) {
int r[5]; readRawSensors(r); uint8_t bb[5]; makeBinaryFromRaw(r,bb);
if (bb[2]) break;
}
optimizePath();
continue;
}

// At intersection: choose according to left-hand or right-hand


if (preferLeftHand) {
if (L) { pivotLeft(); pushMove('L'); }
else if (S) { goStraightShort(); pushMove('S'); }
else if (R) { pivotRight(); pushMove('R'); }
} else {
if (R) { pivotRight(); pushMove('R'); }
else if (S) { goStraightShort(); pushMove('S'); }
else if (L) { pivotLeft(); pushMove('L'); }
}
optimizePath();
}
}

// -------------------- ACTUAL RUN: replay shortest path --------------------


void stepMove(char m) {
if (m == 'L') pivotLeft();
else if (m == 'R') pivotRight();
else if (m == 'S') goStraightShort();
else if (m == 'B') {
setMotor(turnSpeed, true, turnSpeed, false); delay(450);
}
}
void doActualRun() {
// Load path from EEPROM into replay[]
if (!loadPathFromEEPROM()) {
[Link](); [Link]("No path save!");
return;
}
replayLen = pathLen;
for (int i = 0; i < replayLen; ++i) replay[i] = path[i];

[Link](); [Link]("Actual Run");


unsigned long actualStart = millis();

int idx = 0;
while (idx < replayLen) {
// Check end box while following
int raw[5]; readRawSensors(raw);
if (checkEndBox(raw)) {
digitalWrite(RED_LED, HIGH);
stopMotors(); delay(1000); digitalWrite(RED_LED, LOW);
journeyComplete = true;
[Link](); [Link]("Reached End!");
return;
}
// follow until next junction then perform next planned move
bool L,S,R,D; senseBranches(L,S,R,D);
if (L || R || S) {
// execute stored move
char mv = replay[idx++];
stepMove(mv);
// small settle
setMotor(120,true,120,true); delay(80);
continue;
}
// else keep tracking
if (computePositionFromBinary((uint8_t[5]){0,0,0,0,0}) != 999.0f) {
// This cast expression is a small hack; just read actual sensors and follow
normally
int r[5]; readRawSensors(r); uint8_t b[5]; makeBinaryFromRaw(r,b);
float pos = computePositionFromBinary(b);
if (pos != 999.0f) followLinePD(pos);
} else {
// lost: try recover
setMotor(120,false,120,false); delay(120);
}
}

// After replayed moves, go straight to find end box (safety window)


unsigned long chaseT = millis();
while (millis() - chaseT < 8000) {
int raw[5]; readRawSensors(raw);
uint8_t b[5]; makeBinaryFromRaw(raw,b);
float p = computePositionFromBinary(b);
if (checkEndBox(raw)) { digitalWrite(RED_LED, HIGH); stopMotors();
journeyComplete = true; return; }
if (p != 999.0f) followLinePD(p);
else { setMotor(120,true,120,false); delay(120); }
}
// fail-safe stop
stopMotors();
}

// -------------------- AUTOTUNE (simple reactive approach) --------------------


void autoTuneIteration(float curErr) {
float diff = fabs(curErr - lastError);
if (diff > 5.0f) {
// big oscillation -> increase D a bit
Kd += 0.08f;
}
if (fabs(curErr) > 3.0f) {
Kp += 0.06f;
}
// guard limits
Kp = constrain(Kp, 2.0f, 80.0f);
Kd = constrain(Kd, 0.0f, 120.0f);
// Ki left small (0) in high-speed robots
}

// -------------------- START SEQUENCE --------------------


void waitForStartButton() {
// wait for button pressed & released
while (digitalRead(START_BTN) == HIGH) { delay(5); } // assuming INPUT_PULLUP
delay(40);
while (digitalRead(START_BTN) == LOW) { delay(5); }
}

// -------------------- SETUP & LOOP --------------------


void setup() {
// pin modes
pinMode(START_BTN, INPUT_PULLUP);
pinMode(RED_LED, OUTPUT);
pinMode(STATUS_LED, OUTPUT);

pinMode(L_PWM, OUTPUT); pinMode(L_IN1, OUTPUT); pinMode(L_IN2, OUTPUT);


pinMode(R_PWM, OUTPUT); pinMode(R_IN3, OUTPUT); pinMode(R_IN4, OUTPUT);

for (int i = 0; i < 5; ++i) pinMode(SENS_PINS[i], INPUT);

[Link](16,2); [Link]("Meshmerize Bot");


delay(700);

// Clear path if button held during power-on (~2s)


unsigned long startT = millis();
bool held = false;
while (millis() - startT < 2200) {
if (digitalRead(START_BTN) == LOW) { held = true; break; }
}
if (held) {
clearPathEEPROM();
[Link](); [Link]("Path cleared");
for (int i=0;i<3;i++){ digitalWrite(STATUS_LED, HIGH); delay(120);
digitalWrite(STATUS_LED, LOW); delay(120); }
delay(500);
}

// calibrate sensors (one minute allowed by rules: we use short auto-calib)


[Link](); [Link]("Calibrating...");
calibrateSensors();

// load PID if saved


if (loadPIDFromEEPROM()) { [Link](0,1); [Link]("PID loaded");
delay(600); }
else { [Link](0,1); [Link]("PID default"); delay(600); }

// determine if path exists


havePathInEEPROM = loadPathFromEEPROM();
if (havePathInEEPROM) { [Link](); [Link]("Path found"); delay(400); }

[Link](); [Link]("Press Start");


waitForStartButton();

// Start: if path exists -> Actual Run; else -> Dry Run
if (!havePathInEEPROM) {
// Dry Run
autotuneDone = false; autotuneStart = millis();
dryRunStart = millis();
// We'll auto-tune during Dry Run initial period
doDryRun();
// After Dry run we saved optimized path
// Save PID too (if autotuned) - here we run a short autotune: but we can save
if changed
savePIDToEEPROM();
// Indicate ready for actual run (user restarts robot to start actual run)
[Link](); [Link]("Dry Done - Restart");
stopMotors();
while (1) { digitalWrite(STATUS_LED, millis()/300%2); delay(80); } // wait
indefinitely
} else {
// Actual Run
[Link](); [Link]("Actual Run");
doActualRun();
// At the end, glow red LED and blink status LED
if (journeyComplete) {
digitalWrite(RED_LED, HIGH);
for (int i=0;i<10;i++){ digitalWrite(STATUS_LED, HIGH); delay(200);
digitalWrite(STATUS_LED, LOW); delay(200); }
}
stopMotors();
}
}

void loop() {
// nothing; run handled inside setup to use single start button semantics
}

You might also like