Navigate with a Rover Base

The navigation service allows you to queue up user-defined waypoints for your machine to move to in the order that you specify. You can also add obstacles or set linear and angular velocity targets in your navigation service config. Viam’s motion planner will plan routes that avoid those obstacles and attempt to keep the robot at your specified velocity.

To try it out yourself, you need a mobile base and a movement sensor that can track the robot’s GPS coordinates and angular and linear velocity. Follow this tutorial to get started using Viam’s Navigation service to help your wheeled base navigate across space with our recommended stack.

Requirements

  1. A base

    We used a LEO rover, configured as a wheeled base, but you can use whatever model of rover base you have on hand:

    Leo rover that is navigating using the navigation service in a robotics lab.
  2. A movement sensor with GPS position, compass heading, and angular and linear velocity readings

    We used three movement sensors to satisfy these requirements:

    1. A SparkFun GPS-RTK-SMA Breakout movement sensor configured as a gps-nmea-rtk-serial model, providing GPS position and compass heading measurements.
    2. A wheeled-odometry model gathering angular and linear velocity information from the encoders wired to our base’s motors.
    3. A merged model aggregating the readings together for the navigation service to consume.

    You can use any combo of movement sensors you want as long as you are getting all the types of measurements required. See the navigation service for more info on movement sensor requirements.

Before you start, make sure to create a machine in the Viam app and install viam-server on your robot.

Configure the components you need

First, configure the components of your robot.

Click to see how we configured our LEO rover

Configure a board with "digital_interrupts"

First, configure the board local to your rover. Follow these instructions to configure your board model. We used a jetson board, but you can use any model of board you have on hand, as the resource’s API is hardware agnostic.

  1. Configure a board named local as shown below:

Configuration of a jetson board with digital interrupts in the Viam app config builder.

  1. Configure digital interrupts on your board to signal precise GPIO state changes to the encoders on your rover base. Find your board on the CONFIGURE tab in Builder mode. Click the {} (Switch to advanced) button on the right side of your board’s card to switch to JSON attributes editing mode. Copy and paste the following JSON into your board’s attributes field to add digital interrupts on pins 31, 29, 23, and 21:
{
  "digital_interrupts": [
    {
      "name": "ra",
      "pin": "31"
    },
    {
      "pin": "29",
      "name": "rb"
    },
    {
      "pin": "23",
      "name": "lb"
    },
    {
      "name": "la",
      "pin": "21"
    }
  ]
}
  1. Save your config.

Configure a rover base with encoded motors

Configure your rover base to act as the moving platform of the navigating robot. Start by configuring the encoders and motors of your encoded motor.

  1. Follow these instructions to configure the left and right encoders of the wheeled base. We configured ours as incremental encoders, as shown below:

    Configuration of a right incremental encoder in the Viam app config builder. Configuration of a left incremental encoder in the Viam app config builder.

    Assign the pins as the digital interrupts you configured for the board, and wire the encoders accordingly to pins numbered 31, 29, 23, and 21 on your local board. Refer to the incremental encoder documentation for attribute information.

  2. Next, follow these instructions to configure the left and right motors of the wheeled base. We configured ours as gpio motors, as shown below:

Configuration of a right gpio motor in the Viam app config builder. Configuration of a left gpio motor in the Viam app config builder.

Wire the motors accordingly to the GPIO pins numbered 35, 35, 15, 38, 40, and 33 on your local board. Refer to the gpio motor documentation for attribute information.

  1. Finally, configure whatever rover you have as a wheeled model of base, bringing the motion produced by these motors together on one platform:

    An example configuration for a wheeled base in the Viam app Config Builder.
    • Make sure to select each of your right and left motors as right and left, as well as set the wheel_circumference_mm and width_mm of each of the wheels the motors are attached to.

    • Configure the frame system for this wheeled base so that the navigation service knows where it is in relation to the movement sensor.

      • Switch to Frame mode on the CONFIGURE tab and select your base. If your movement sensor is mounted on top of the rover like ours is, set Orientation’s third input field, Z, to 1 and its fourth input field, theta, to 90.

      • Select the world as the parent frame.

        An example configuration for a wheeled base in the Viam app Frame System.

    Refer to the wheeled base configuration instructions for attribute information.

In the JSON mode in your machine’s CONFIGURE tab, add the following JSON objects to the "components" array:

    {
      "depends_on": [],
      "model": "jetson",
      "name": "local",
      "type": "board",
      "attributes": {
        "digital_interrupts": [
          {
            "name": "ra",
            "pin": "31"
          },
          {
            "pin": "29",
            "name": "rb"
          },
          {
            "pin": "23",
            "name": "lb"
          },
          {
            "name": "la",
            "pin": "21"
          }
        ]
      }
    },
    {
      "attributes": {
        "width_mm": 350,
        "left": [
          "left-motor"
        ],
        "right": [
          "right-motor"
        ],
        "wheel_circumference_mm": 400
      },
      "depends_on": [
        "left-motor",
        "right-motor"
      ],
      "frame": {
        "translation": {
          "y": 0,
          "z": 0,
          "x": 0
        },
        "orientation": {
          "value": {
            "y": 0,
            "z": 1,
            "th": 90,
            "x": 0
          },
          "type": "ov_degrees"
        },
        "parent": "world"
      },
      "model": "wheeled",
      "name": "base",
      "type": "base"
    },
    {
      "model": "gpio",
      "name": "left-motor",
      "type": "motor",
      "attributes": {
        "ticks_per_rotation": 420,
        "board": "local",
        "max_rpm": 50,
        "pins": {
          "pwm": "33",
          "a": "38",
          "b": "40"
        },
        "encoder": "l-encoder"
      },
      "depends_on": []
    },
    {
      "model": "gpio",
      "name": "right-motor",
      "type": "motor",
      "attributes": {
        "encoder": "r-encoder",
        "ticks_per_rotation": 425,
        "max_rpm": 50,
        "pins": {
          "a": "35",
          "b": "36",
          "pwm": "15"
        },
        "board": "local"
      },
      "depends_on": []
    },
    {
      "attributes": {
        "board": "local",
        "pins": {
          "b": "lb",
          "a": "la"
        }
      },
      "depends_on": [],
      "name": "l-encoder",
      "type": "encoder",
      "model": "incremental"
    },
    {
      "model": "incremental",
      "type": "encoder",
      "namespace": "rdk",
      "attributes": {
        "board": "local",
        "pins": {
          "b": "rb",
          "a": "ra"
        }
      },
      "depends_on": [],
      "name": "r-encoder"
    }

Configure movement sensors

  1. Configure a GPS movement sensor so the robot knows where it is while navigating. We configured ours as a gps-nmea-rtk-serial movement sensor:

    An example configuration for a GPS movement sensor in the Viam app Config Builder.

    We named ours gps. Refer to the gps-nmea-rtk-serial movement sensor documentation for attribute information.

  2. Configure a wheeled odometry movement sensor to provide angular and linear velocity measurements from the encoded motors on our base.

    An example configuration for a wheeled-odometry movement sensor in the Viam app Config Builder.

    We named ours enc-linear. Refer to the wheeled-odometry movement sensor documentation for attribute information.

  3. Now that you’ve got movement sensors which can give you GPS position and linear and angular velocity readings, configure a merged movement sensor to aggregate the readings from our other movement sensors into a singular sensor:

    An example configuration for a merged movement sensor in the Viam app Config Builder.

    We named ours merged. Refer to the merged movement sensor documentation for attribute information.

    • Make sure your merged movement sensor is configured to gather "position" readings from the gps movement sensor.

    • Configure the frame system for this movement sensor so that the navigation service knows where it is in relation to the base.

      • Switch to Frame mode on the CONFIGURE tab and select your movement sensor. If your movement sensor is mounted on top of the rover like ours is, set Orientation’s third input field, Z, to 1.

      • Select the base as the parent frame.

        An example configuration for a merged movement sensor in the Viam app Frame System.

In the JSON mode in your machine’s CONFIGURE tab, add the following JSON objects to the "components" array:

    {
      "name": "gps",
      "type": "movement_sensor",
      "attributes": {
        "ntrip_password": "yourpassword",
        "ntrip_url": "http://your.url:8082",
        "ntrip_username": "yourusername",
        "serial_baud_rate": 115200,
        "serial_path": "/dev/serial/by-id/usb-u-blox_AG_-_www.u-blox.com_u-blox_GNSS_receiver-if00",
        "ntrip_connect_attempts": 10,
        "ntrip_mountpoint": "NJI2"
      },
      "depends_on": [],
      "model": "gps-nmea-rtk-serial"
    },
    {
      "name": "merged",
      "type": "movement_sensor",
      "attributes": {
        "angular_velocity": [
          "enc-linear"
        ],
        "compass_heading": [
          "gps"
        ],
        "orientation": [
          "enc-linear"
        ],
        "position": [
          "gps"
        ]
      },
      "depends_on": [],
      "frame": {
        "orientation": {
          "value": {
            "z": 1,
            "th": 0,
            "x": 0,
            "y": 0
          },
          "type": "ov_degrees"
        },
        "parent": "base",
        "translation": {
          "y": 0,
          "z": 0,
          "x": 0
        }
      },
      "model": "merged"
    },
    {
      "model": "wheeled-odometry",
      "type": "movement_sensor",
      "namespace": "rdk",
      "attributes": {
        "base": "base",
        "left_motors": [
          "left-motor"
        ],
        "right_motors": [
          "right-motor"
        ]
      },
      "depends_on": [],
      "name": "enc-linear"
    }

Configure a navigation service

Add the navigation service so that your wheeled base can navigate between waypoints and avoid obstacles. To add the navigation service to your robot, do the following:

  1. Navigate to the CONFIGURE tab of your machine’s page in the Viam app.

  2. Click the + icon next to your machine part in the left-hand menu and select Service.

  3. Select the navigation type.

  4. Enter a name or use the suggested name for your service and click Create.

  5. Select JSON mode. Copy and paste the following into your new service’s attributes field:

    {
      "base": "base",
      "movement_sensor": "merged",
      "obstacles": [],
      "store": {
        "type": "memory"
      },
      "position_polling_frequency": 2,
      "meters_per_sec": 1.2,
      "degs_per_sec": 90,
      "plan_deviation_m": 0.25
    }
    

    Edit the attributes as applicable. Attribute information is available in the navigation service documentation.

  6. Click Save in the top right corner of the screen to save your changes.

Your navigation service should now appear in your machine’s CONFIGURE tab as a card with a map like the following:

Navigation Card

For more detailed information see the navigation service.

In JSON mode in your machine’s CONFIGURE tab, add the following JSON object to the "services" array:

"services": [
  {
    "name": "nav",
    "type": "navigation",
    "attributes": {
    "base": "base",
    "movement_sensor": "merged",
    "obstacles": [],
    "store": {
        "type": "memory"
    },
    "position_polling_frequency": 2,
    "meters_per_sec": 1.2,
    "degs_per_sec": 90,
    "plan_deviation_m": 0.25
    }
  }
]

Click Save in the top right corner of the screen to save your changes.

Start navigating with the navigation service

Now that you have configured your navigation service, add waypoints to your navigation service. You can add waypoints from the CONTROL tab or programmatically.

Control tab method

Go to the CONTROL tab of your robot in the Viam app, and open the navigation service card.

From there, ensure that Navigation mode is selected as Manual, so your robot will not begin navigation while you add waypoints.

Add waypoints

Select Waypoints on the upper-left corner menu of the navigation card. Zoom in on your current location and click on the map to add a waypoint.

Waypoint 0 being added in the Viam app config builder on a New York City street

Add as many waypoints as you desire. Hover over a waypoint in the left-hand menu and click the trash icon to delete a waypoint.

Waypoint 1 being added in the Viam app config builder, further down the street

(Optional) Add obstacles

If you want your robot to avoid certain obstacles in its path while navigating, you can also add obstacles. In the CONFIGURE tab, select the Obstacles subtab on the navigation card. Zoom in on your current location, then hold shift and drag on the map to draw an obstacle. Add as many obstacles as you desire. Hover over an obstacle in the left-hand menu and click the trash icon to delete an obstacle.

Begin navigation

Toggle Navigation mode to Waypoint. Your rover will begin navigating between waypoints.

Programmatic method

If you want to do add waypoints programmatically, use the service’s API method AddWaypoint():

Add waypoints

myNav, err := navigation.FromRobot(robot, "my_nav_service")

// Create a new waypoint at the specified latitude and longitude
location = geo.NewPoint(40.76275, -73.96)

// Add your waypoint to the service's data storage
err := myNav.AddWaypoint(context.Background(), location, nil)
my_nav = NavigationClient.from_robot(robot=robot, name="my_nav_service")

# Create a new waypoint at the specified latitude and longitude
location = GeoPoint(latitude=40.76275, longitude=-73.96)

# Add your waypoint to the service's data storage
await my_nav.add_waypoint(point=location)

Begin navigation

To start navigating, set your service to MODE_WAYPOINT with the service’s API method SetMode():

myNav, err := navigation.FromRobot(robot, "my_nav_service")

// Set the Mode the service is operating in to MODE_WAYPOINT and begin navigation
mode, err := myNav.SetMode(context.Background(), Mode.MODE_WAYPOINT, nil)
my_nav = NavigationClient.from_robot(robot=robot, name="my_nav_service")

# Set the Mode the service is operating in to MODE_WAYPOINT and begin
# navigation
await my_nav.set_mode(Mode.ValueType.MODE_WAYPOINT)

Next steps: automate obstacle detection

In this tutorial, you have learned how to use Navigation to navigate across waypoints. Now, you can make navigation even better with automated obstacle detection.

First, configure a depth camera that your robot can sense how far away it is from obstacles.

We configured ours as an Intel RealSense Camera, which is available as a modular resource in the Viam registry:

An example configuration for an Intel RealSense camera in the Viam app Config Builder.

If you want the robot to be able to automatically detect obstacles in front of it, configure a Vision service segmenter. For example, configure the Vision service model obstacles_depth to detect obstacles in front of the robot. Then, use one of Viam’s client SDKs to automate obstacle avoidance with the navigation service like in the following Python program:

Click to view full example of automated obstacle avoidance with the Python SDK
import asyncio
import time
from viam.robot.client import RobotClient
from viam.rpc.dial import Credentials, DialOptions
from viam.components.base import Base
from viam.services.vision import VisionClient
from viam.proto.common import GeoPoint
from viam.services.navigation import NavigationClient

MANUAL_MODE = 1
DRIVE_MODE = 2
SECONDS_TO_RUN = 60 * 15


async def connect():
    opts = RobotClient.Options.with_api_key(
        # Replace "<API-KEY>" (including brackets) with your machine's API key
        api_key='<API-KEY>',
        # Replace "<API-KEY-ID>" (including brackets) with your machine's
        # API key ID
        api_key_id='<API-KEY-ID>'
    )
    return await RobotClient.at_address('<INSERT REMOTE ADDRESS>', opts)


async def nav_avoid_obstacles(
    base: Base,
    nav_service: NavigationClient,
    obstacle_detection_service: VisionClient
):
    while True:
        obstacle = await obstacle_detection_service.get_object_point_clouds(
          "myRealSense"
        )
        print("obstacle: ", obstacle)
        z = obstacle[0].geometries.geometries[0].center.z
        print(z)
        r = await nav_service.get_mode()
        if z < 1000:
            if r != MANUAL_MODE:
                await nav_service.set_mode(MANUAL_MODE)
        else:
            if r != DRIVE_MODE:
                await nav_service.set_mode(DRIVE_MODE)


async def main():
    robot = await connect()

    # Get base component and services from the robot
    base = Base.from_robot(robot, "base")
    obstacle_detection_service = VisionClient.from_robot(robot, "myObsDepth")
    nav_service = NavigationClient.from_robot(robot, "nav")

    # Get waypoints and add a new waypoint
    waypoints = await nav_service.get_waypoints()
    assert (len(waypoints) == 0)
    await nav_service.add_waypoint(GeoPoint(latitude=0.00006, longitude=0))

    # Get waypoints again, check to see that one has been added
    waypoints = await nav_service.get_waypoints()
    assert (len(waypoints) == 1)

    # Avoid obstacles
    await nav_avoid_obstacles(base, nav_service, obstacle_detection_service)

if __name__ == '__main__':
    asyncio.run(main())

You can also ask questions in the Community Discord and we will be happy to help.