
Sooner or later every enthusiast programs a Lego Mindstorms Ev3 robot to follow a line. Indeed the line following task is the first that I tried myself, and there is a post on this blog where I show the results obtained with the Sup3r Car.
Here I show how to program the SNATCH3R to follow a line with C# and the Monobrick firmware library; the main difference between the Sup3r Car and the SNATCH3R program is that the Sup3r Car uses a PID (Proportional Integrative Derivative) controller to stay on the track, while the SNATCH3R uses a much simpler look up table.
You can download the code from Smallrobots.it repository on GitHub.
The line following task for the SNATCH3R
The task consists in programming an Ev3 robot to follow a line on the floor usually marked with a coloured tape; to sense the line on the floor the robot uses an Ev3 Color Sensor in Reflection Mode.
The SNATCH3R carries also an Ev3 IR Sensor set in proximity mode to detect the obstacles along the line. Once an obstacle is detected, the robot uses the gripper to grab and remove it.
The state machine

The SNATCH3R state machine has the following states:
: Follow the line until an obstacle is found
: Grab the obstacle
: Turn right
: Move forward
: Release the obstacle
: Move backward
: Turn left and when done get back to state
.
This state machine is very simple in the sense that it is just a sequence. Once the only action inside a state is completed, the current state changes to the following one in the sequence.
The only exception to the rule above is the state: the robot changes from
to
only when the Ev3 IR Sensor detects an obstacle along the way.
The states of the state machine are defined as follows:
1 2 3 4 5 6 7 8 9 10 11 |
public enum LineFollowingTask_States { starting = 0, lineFollowing, grabbingObstacle, rotating, advancing, deliveringObstacle, retracting, counterRotating } |
Each state has a dedicated method which updates the robot behaviour, so the state machine looks clean:
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 |
private void OnTimer(Robot robot) { switch (currentState) { case LineFollowingTask_States.starting: Buttons.LedPattern(1); Starting_Case(); break; case LineFollowingTask_States.lineFollowing: Buttons.LedPattern(1); LineFollowing_Case(robot); break; case LineFollowingTask_States.grabbingObstacle: Buttons.LedPattern(2); GrabbingObstacle_Case(robot); break; case LineFollowingTask_States.rotating: Buttons.LedPattern(2); Rotating_Case(robot); break; case LineFollowingTask_States.advancing: Buttons.LedPattern(2); Advancing_Case(robot); break; case LineFollowingTask_States.deliveringObstacle: Buttons.LedPattern(2); DeliveringObstacle_Case(robot); break; case LineFollowingTask_States.retracting: Buttons.LedPattern(3); Retracting_Case(robot); break; case LineFollowingTask_States.counterRotating: Buttons.LedPattern(3); CounterRotating_Case(robot); break; default: currentLeftPower = 0; currentRightPower = 0; currentGripperPower = 0; break; } // Update the robot current status for display purposes ((Snatch3r)robot).currentLineFollowingState = currentState; // Send the updated set point to the legs controller ((Snatch3r)robot).leftMotor.SetPower(currentLeftPower); ((Snatch3r)robot).rightMotor.SetPower(currentRightPower); ((Snatch3r)robot).gripperMotor.SetPower(currentGripperPower); } |
Follow the line
To stay on the track the robot must sense the line on the floor with the Ev3 Color Sensor; the sensor feedback is then compared to a predefined Set-Point value. The Set-Point value is the value that the Ev3 Color Sensor reads when the red light it emits is half on the line and half on the floor.
The algorithm is simple: if the difference between the Set-Point and the sensor feedback is positive the robot must steer toward the line, while if the sensor feedback is negative the robot must steer toward the floor. With this statement I’m assuming that the line is darker than the floor.

To avoid sharp turn, the amount of correction is not constant but depends on how far the robot is from the Set-Point: if the feedback is close to the Set-Point the robot makes a small correction, while if the feedback is far from the Set-Point the robot makes a greater correction to its course.
Have a look at the table on the left for the values used in the program proposed. For the black tape I used, and the floor in my home a good value for the SetPoint is 40, however these values should be tuned for your environment condition.
In the following code, observe also how the current state changes when an obstacle is detected by the Ev3 IR Sensor.
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 |
private void LineFollowing_Case(Robot robot) { Snatch3r snatcher = (Snatch3r)robot; if (previousState != LineFollowingTask_States.lineFollowing) { // Initialize the state setPoint = 40; threshold = 10; Thread soundThread = new Thread(new ThreadStart(() => { snatcher.speaker.PlaySoundFile("/home/root/apps/Snatch3r/trucks001.wav",250); })); soundThread.Start(); previousState = LineFollowingTask_States.lineFollowing; } // Give inputs to the look up table colorSensorReading = snatcher.colorSensor.Read(); snatcher.steering = (sbyte)lookUpTable.GetValue(colorSensorReading); currentLeftPower = (sbyte) (forwardPower + 0.5*snatcher.steering); currentRightPower = (sbyte) (forwardPower - 0.5* snatcher.steering); // Checks for obstacles int targetDistance = snatcher.irSensor.ReadDistance(); if ((targetDistance < minDistanceFromObstacle) && (targetDistance>0)) { currentLeftPower = 0; currentRightPower = 0; currentState = LineFollowingTask_States.grabbingObstacle; } } |
The look up table is a simple class which performs a linear search in an array as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public double GetValue(double theKey) { double retValue = 0.0d; for (int i = 0; i < lookUpTable.Count; i++) { if ((theKey >= lookUpTable[i].MinExtreme) && (theKey < lookUpTable[i].MaxExtreme)) { retValue = lookUpTable[i].Value; break; } } return retValue; } |
Grab the obstacle
The SNATCH3R performs this action in open loop: it simply rotates the Ev3 Medium Motor until its tachimeter count equals or exceeds 3500 hits, then the current state is changed to turn right.
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 |
private void GrabbingObstacle_Case(Robot robot) { Snatch3r snatcher = (Snatch3r)robot; if (previousState != LineFollowingTask_States.grabbingObstacle) { previousState = LineFollowingTask_States.grabbingObstacle; snatcher.gripperMotor.ResetTacho(); // This function does nothing, body is completely commented into the library snatcher.speaker.StopSoundPlayback(); Thread.Sleep(500); Thread soundThread = new Thread(new ThreadStart(() => { snatcher.speaker.PlaySoundFile("/home/root/apps/Snatch3r/BeepBeep.wav", 250); })); soundThread.Start(); } int gripperTachoCount = ((Snatch3r)robot).gripperMotor.GetTachoCount(); if (gripperTachoCount < 3500) { currentGripperPower = gripperPower; } else { currentGripperPower = 0; currentState = LineFollowingTask_States.rotating; } } |
Turn right
The action is performed in open loop. The two caterpillars move in opposite direction with equal speed until the left Ev3 Large Motor tacho count equals or exceeds 400. Actually this method turns the robot once to the left and once to the right for each obstacle encountered.
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 |
"]private void Rotating_Case(Robot robot) { Snatch3r snatcher = (Snatch3r)robot; if (previousState != LineFollowingTask_States.rotating) { previousState = LineFollowingTask_States.rotating; snatcher.leftMotor.ResetTacho(); } if (!thisTimeToLeft) { currentLeftPower = (sbyte)turnPower; currentRightPower = (sbyte)-turnPower; if (snatcher.leftMotor.GetTachoCount() > halfSwipe) { currentLeftPower = 0; currentRightPower = 0; currentState = LineFollowingTask_States.advancing; } } else { currentLeftPower = (sbyte)-turnPower; currentRightPower = (sbyte)turnPower; if (snatcher.leftMotor.GetTachoCount() < -halfSwipe) { currentLeftPower = 0; currentRightPower = 0; currentState = LineFollowingTask_States.advancing; } } } |
Move forward
This state contains an action performed in open loop. Both Ev3 Large Motors move forward until the left tacho count reaches or exceeds 400 (same value as turn right).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
private void Advancing_Case(Robot robot) { Snatch3r snatcher = (Snatch3r)robot; if (previousState != LineFollowingTask_States.advancing) { previousState = LineFollowingTask_States.advancing; snatcher.leftMotor.ResetTacho(); } currentLeftPower = (sbyte)forwardPower; currentRightPower = (sbyte)forwardPower; if (snatcher.leftMotor.GetTachoCount() > halfSwipe) { currentLeftPower = 0; currentRightPower = 0; currentState = LineFollowingTask_States.deliveringObstacle; } } |
Release the obstacle
The SNATCH3R performs this action in open loop: the Ev3 Medium Motor rotates until its tachimeter count returns to zero, then the current state is changed to move backward.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private void DeliveringObstacle_Case(Robot robot) { Snatch3r snatcher = (Snatch3r)robot; if (previousState != LineFollowingTask_States.deliveringObstacle) { previousState = LineFollowingTask_States.deliveringObstacle; snatcher.leftMotor.ResetTacho(); } int gripperTachoCount = snatcher.gripperMotor.GetTachoCount(); if (gripperTachoCount > 0) { currentGripperPower = (sbyte)-gripperPower; } else { currentGripperPower = 0; currentState = LineFollowingTask_States.retracting; } } |
Move backward
The SNATCH3R performs this action in open loop: the two Ev3 Large Motors rotate until left motor tachimeter count equals or exceeds -400 hits, then the current state is changed to turn left.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
private void Retracting_Case(Robot robot) { Snatch3r snatcher = (Snatch3r)robot; if (previousState != LineFollowingTask_States.retracting) { previousState = LineFollowingTask_States.retracting; snatcher.leftMotor.ResetTacho(); } currentLeftPower = (sbyte)-backwardPower; currentRightPower = (sbyte)-backwardPower; if (snatcher.leftMotor.GetTachoCount() < -halfSwipe) { currentLeftPower = 0; currentRightPower = 0; currentState = LineFollowingTask_States.counterRotating; } } |
Turn left
The action is performed in open loop. The two caterpillars move in opposite direction with equal speed until the robot is back on track. To give the Ev3 Color Sensor a bit of tolerance in finding the black tape again, the SNATCH3R counter rotate 20% less than the amount of initial rotation. After that the state machine is back again to the one
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 |
private void CounterRotating_Case(Robot robot) { Snatch3r snatcher = (Snatch3r)robot; if (previousState != LineFollowingTask_States.counterRotating) { previousState = LineFollowingTask_States.counterRotating; snatcher.leftMotor.ResetTacho(); } if (!thisTimeToLeft) { currentLeftPower = (sbyte)-turnPower; currentRightPower = (sbyte)+turnPower; if (snatcher.leftMotor.GetTachoCount() < -0.8 * halfSwipe) { currentLeftPower = 0; currentRightPower = 0; // Back to line following currentState = LineFollowingTask_States.lineFollowing; thisTimeToLeft = !thisTimeToLeft; } } else { currentLeftPower = (sbyte)turnPower; currentRightPower = (sbyte)-turnPower; if (snatcher.leftMotor.GetTachoCount() > 0.8 * halfSwipe) { currentLeftPower = 0; currentRightPower = 0; // Back to line following currentState = LineFollowingTask_States.lineFollowing; thisTimeToLeft = !thisTimeToLeft; } } } |
Happy coding!
Once an obstacle is detected, the robot uses the gripper to grab and remove it.