An autonomous lawn-mowing robot built on a custom differential-drive platform in ROS and simulated in Gazebo. Give it the size of the lawn and a number of laps, and it plans its own boustrophedon coverage route, drives it, and steers around obstacles using a Hokuyo LiDAR — then rejoins the route and carries on.
The robot rounding an obstacle it has never seen before. Nothing is mapped in advance — the detour comes entirely from the live LiDAR scan.
The brief was to cover an entire bounded area with a differential-drive robot, with the boundary supplied as input. That splits cleanly into two problems that fight each other: following a planned route, and reacting to things the route knows nothing about. Solving them in one controller tends to produce a robot that either ignores obstacles or wanders off and never finishes the lawn.
Here they are two separate ROS nodes with a service between them, so exactly one is driving at any moment — and, crucially, a geometric condition that decides when the obstacle-avoider gives control back.
| Driving the route | Avoiding an obstacle |
|---|---|
![]() |
![]() |
| With a clear path the robot heads straight for the next waypoint. | When the front LiDAR region sees something within 1 m, the wall follower takes over. |
The planned sweep across the lawn:
Vertical passes one unit apart, alternating direction on each pass, repeated for the requested number of laps.
flowchart LR
G[Gazebo simulation] -->|/scan| P[go_to_point<br/>project.py]
G -->|/odom| P
G -->|/scan| W[wall_follower<br/>follow_wall.py]
P -->|/cmd_vel| G
W -->|/cmd_vel| G
P -.->|/wall_follower_switch<br/>std_srvs/SetBool| W
1. Coverage planning (project.py). From the lawn length, width and lap count, goals()
generates the boustrophedon waypoint list — up column 1, down column 2, up column 3, and so on,
with the direction reversed on each successive lap. Consecutive duplicate points are collapsed so
the path stays clean.
2. Waypoint driving. A four-state machine walks that list:
| State | Behaviour |
|---|---|
0 fix yaw |
Rotate in place until the heading points at the next waypoint (tolerance π/90) |
1 go straight |
Drive at 0.2 m/s with a proportional yaw correction, until within 0.1 m of the waypoint |
2 wall following |
Hand control to the wall follower |
3 done |
Stop and exit |
3. Obstacle avoidance (follow_wall.py). The 720-sample LiDAR scan is reduced to five angular
regions — right, fright, front, fleft, left — each the minimum range in its sector. A
1.5 m threshold selects one of three behaviours: find the wall, turn left, or follow the
wall, keeping the obstacle to one side while circling it.
4. Handing control back. This is the part that makes the whole thing terminate. Re-entry is decided by the perpendicular distance from the robot to the straight line joining the last waypoint to the next one; once that drops below 0.1 m the planner takes back over. It is the classic Bug2 leave condition, and without it an obstacle detour has no principled end — the robot would either rejoin too early and re-collide, or keep following the obstacle's contour forever.
Defined in lawn_mover/urdf/lawn_mover.urdf:
- Differential drive — left and right driven wheels plus a front caster, via
libgazebo_ros_diff_drive.so - Hokuyo LiDAR on
hokuyo_link— 720 samples across a 180° field of view (−π/2 to π/2), 30 m maximum range, vialibgazebo_ros_laser.so. Top-mounted, so obstacles cannot strike the sensor. - Joint states published by
libgazebo_ros_joint_state_publisher.so
The lawn is a custom Gazebo world,
lawn_mover/worlds/lawn_world.world.
- Middleware: ROS 1 (rospy, roslaunch, ROS services)
- Simulation: Gazebo, URDF robot description, custom
.world - Sensing: Hokuyo LiDAR via
sensor_msgs/LaserScan, odometry vianav_msgs/Odometry - Control:
geometry_msgs/Twiston/cmd_vel - Build: catkin (
CMakeLists.txt,package.xml)
LawnMoving-Robot-Using-ROS/
├── lawn_mover/ # the ROS (catkin) package
│ ├── src/
│ │ ├── project.py # go_to_point: coverage planning + waypoint driving
│ │ └── follow_wall.py # wall_follower: LiDAR obstacle avoidance
│ ├── launch/
│ │ └── lawn_mover.launch # Gazebo + world + robot + wall follower
│ ├── urdf/
│ │ ├── lawn_mover.urdf # differential-drive robot with Hokuyo LiDAR
│ │ └── lawn_mover2.urdf
│ ├── worlds/
│ │ └── lawn_world.world # custom Gazebo lawn with obstacles
│ ├── CMakeLists.txt
│ └── package.xml
├── docs/
│ ├── ROS-REPORT.docx # full project report
│ └── ROSPPT.pptx # project presentation
├── assets/ # figures used in this README
├── README.md
└── LICENSE
Requires ROS 1 (Melodic or Noetic) on Linux with Gazebo, plus gazebo_ros, rospy,
std_srvs, nav_msgs, sensor_msgs, geometry_msgs and tf.
-
Copy the package into a catkin workspace and build it:
cp -r lawn_mover ~/catkin_ws/src/ cd ~/catkin_ws && catkin_make && source devel/setup.bash
-
Launch Gazebo with the world, the robot, and the wall-following node:
roslaunch lawn_mover lawn_mover.launch
-
In a second sourced terminal, start the coverage planner:
rosrun lawn_mover project.py
It prompts for the lawn dimensions:
Enter Length of lawn : 8 Enter Width of lawn : 8 Enter number of laps : 1
The robot prints each waypoint as it reaches it and stops with Process Finished once the route
is complete.
Note — the launch file starts only
follow_wall.py.project.pyruns separately because it needs an interactive terminal for the lawn dimensions.
None. This is a simulation and control project — the robot acts on live LiDAR and odometry from Gazebo, so there is nothing to download.
- Replace the fixed rectangular sweep with coverage of arbitrary polygonal boundaries.
- Add SLAM so the robot builds and reuses a map of static obstacles across laps.
- Track battery state and plan a return-to-base leg before it runs out.
Built for 19AIE213 – Robotic Operating System and Robot Simulation at Amrita School of Engineering, Bangalore (Amrita Vishwa Vidyapeetham), 2021, guided by Mr. Nippun Kumaar A. A., by Vishnu Sainadh Kedarisetty, Satwik Kukkadapu and Ashrith Vadde.


