Drive a base directly from code

A wheeled base does not always need navigation. A demo that drives in a square, an operator joystick that sets velocity in real time, or a sensor-driven control loop that adjusts power on the fly all call for commanding the base directly rather than planning a route to it. The base component API gives you four methods for these cases: MoveStraight and Spin for known-distance moves, SetPower and SetVelocity for continuous control.

For autonomous waypoint following or SLAM-based navigation, use the navigation service instead. See Navigate to a waypoint and MoveOnMap.

Before you start

  • A configured base component. See the base component documentation for hardware setup.
  • The base’s motors run through the TEST section on each motor’s configure card (press and hold forward or backward under Quick move). If a motor’s Test section shows Resource is configuring…, wait for the motor’s status badge to read Ready before attempting Quick move.
  • An SDK client connected to your machine. See Create a web app or the language-specific quickstart.

Direct motion commands

The base component API provides four motion methods:

MethodWhat it does
MoveStraightDrive forward or backward a given distance at a given speed. Blocks until done.
SpinRotate in place by a given angle at a given angular speed. Blocks until done.
SetPowerSet linear and angular power as normalized values (-1.0 to 1.0). Non-blocking.
SetVelocitySet linear and angular velocity directly (mm/s and deg/s). Non-blocking.

Use MoveStraight and Spin when you know the distance or angle you want. Use SetPower or SetVelocity for continuous control (for example, an operator stick, or a sensor-driven control loop).

Drive a base in a square

This example uses MoveStraight and Spin to walk a base around a 500 mm square, stopping at each corner to turn 90 degrees.

import asyncio

from viam.components.base import Base
from viam.robot.client import RobotClient


async def connect():
    opts = RobotClient.Options.with_api_key(
        api_key="YOUR-API-KEY",
        api_key_id="YOUR-API-KEY-ID",
    )
    return await RobotClient.at_address("YOUR-MACHINE-ADDRESS", opts)


async def drive_square(base):
    for _ in range(4):
        # Drive forward 500 mm at 500 mm/s.
        await base.move_straight(distance=500, velocity=500)
        # Spin 90 degrees to the left at 100 deg/s.
        await base.spin(angle=90, velocity=100)


async def main():
    async with await connect() as machine:
        my_base = Base.from_robot(machine, "my-base")
        await drive_square(my_base)


if __name__ == "__main__":
    asyncio.run(main())
package main

import (
    "context"

    "go.viam.com/rdk/components/base"
    "go.viam.com/rdk/logging"
    "go.viam.com/rdk/robot/client"
    "go.viam.com/rdk/utils"
)

func driveSquare(ctx context.Context, b base.Base, logger logging.Logger) {
    for i := 0; i < 4; i++ {
        // Drive forward 500 mm at 500 mm/s.
        if err := b.MoveStraight(ctx, 500, 500.0, nil); err != nil {
            logger.Fatal(err)
        }
        // Spin 90 degrees to the left at 100 deg/s.
        if err := b.Spin(ctx, 90, 100.0, nil); err != nil {
            logger.Fatal(err)
        }
    }
}

func main() {
    logger := logging.NewLogger("client")
    ctx := context.Background()

    machine, err := client.New(
        ctx,
        "YOUR-MACHINE-ADDRESS",
        logger,
        client.WithDialOptions(utils.WithEntityCredentials(
            "YOUR-API-KEY-ID",
            utils.Credentials{
                Type:    utils.CredentialsTypeAPIKey,
                Payload: "YOUR-API-KEY",
            },
        )),
    )
    if err != nil {
        logger.Fatal(err)
    }
    defer machine.Close(ctx)

    myBase, err := base.FromProvider(machine, "my-base")
    if err != nil {
        logger.Fatal(err)
    }

    driveSquare(ctx, myBase, logger)
}

Replace my-base with your base component’s name, and replace the credentials and address placeholders with the values from the CONNECT tab in the Viam app.

Set continuous power or velocity

For operator-driven control or sensor-driven loops, use SetPower or SetVelocity rather than MoveStraight/Spin. Both methods take a linear vector and an angular vector.

For wheeled bases, only two of the six fields matter in practice:

Vector fieldMeaning
linear.YForward and backward. Positive Y drives forward.
angular.ZYaw. Positive Z turns left (counterclockwise, viewed above).

The other four fields (linear.X, linear.Z, angular.X, angular.Y) exist for bases with more degrees of freedom. Built-in wheeled base drivers ignore them.

SetPower values range from -1.0 to 1.0 (fraction of maximum). SetVelocity takes linear velocity in millimeters per second and angular velocity in degrees per second.

from viam.components.base import Base, Vector3

my_base = Base.from_robot(machine, "my-base")

# Drive forward at 75% power with no rotation.
await my_base.set_power(linear=Vector3(x=0, y=0.75, z=0),
                        angular=Vector3(x=0, y=0, z=0))

# Or drive at a fixed velocity: 50 mm/s forward, 15 deg/s left yaw.
await my_base.set_velocity(linear=Vector3(x=0, y=50, z=0),
                           angular=Vector3(x=0, y=0, z=15))

# Stop.
await my_base.stop()
import (
    "github.com/golang/geo/r3"
    "go.viam.com/rdk/components/base"
)

myBase, err := base.FromProvider(machine, "my-base")
if err != nil {
    logger.Fatal(err)
}

// Drive forward at 75% power.
if err := myBase.SetPower(ctx,
    r3.Vector{X: 0, Y: 0.75, Z: 0},
    r3.Vector{X: 0, Y: 0, Z: 0},
    nil); err != nil {
    logger.Fatal(err)
}

// Or drive at fixed velocity: 50 mm/s forward, 15 deg/s left yaw.
if err := myBase.SetVelocity(ctx,
    r3.Vector{X: 0, Y: 50, Z: 0},
    r3.Vector{X: 0, Y: 0, Z: 15},
    nil); err != nil {
    logger.Fatal(err)
}

// Stop.
if err := myBase.Stop(ctx, nil); err != nil {
    logger.Fatal(err)
}

When to use direct commands instead of motion planning

PathUse whenTrade-off
Direct commandsShort, well-defined movements with no obstacles to avoidNo path checking; you are responsible for the route
MoveOnGlobeAutonomous GPS waypoint navigationNeeds a movement sensor with GPS
MoveOnMapDriving to a pose on a SLAM-generated mapNeeds a SLAM service

Details for both planned paths: Navigate to a waypoint and MoveOnMap.

What’s next