Drive a rover in a square in 2 minutes
In this guide you’ll write code that makes a rover drive in a square.
You will learn
- How to run control code with Viam SDKs
- How to use the base API to move a rover in a square
Requirements
You don’t need to buy or own any hardware to complete this tutorial. You only need the following:
- A Linux, macOS or Windows computer that can run SDK code.
- A borrowed Viam Rover, your own Viam Rover, or another mobile robot. You can use Try Viam to borrow a rover online at no cost which is already configured with all the components you need. If you have your own rover on hand, whether it’s a Viam rover or not, these instructions work for any wheeled robot that can be configured as a base component.
Instructions
Follow these steps to get your rover ready and write code to control it:
Important
If you are using your own robot for this tutorial instead of borrowing one, be sure to follow the setup instructions and install viam-server on it, and connect and configure its hardware before proceeding with this tutorial.
Complete code
This is the complete code for the tutorial:
import asyncio
from viam.components.base import Base
from viam.robot.client import RobotClient
from viam.rpc.dial import Credentials, DialOptions
async def connect():
    opts = RobotClient.Options.with_api_key(
        # TODO: Replace "<API-KEY>" (including brackets) with your machine's
        # API key
        api_key='<API-KEY>',
        # TODO: Replace "<API-KEY-ID>" (including brackets) with your machine's
        # API key ID
        api_key_id='<API-KEY-ID>'
    )
    # TODO: Replace "<MACHINE-ADDRESS>" with address from the CONNECT tab.
    return await RobotClient.at_address("<MACHINE-ADDRESS>", opts)
async def moveInSquare(base):
    for _ in range(4):
        # moves the rover forward 500mm at 500mm/s
        await base.move_straight(velocity=500, distance=500)
        print("move straight")
        # spins the rover 90 degrees at 100 degrees per second
        await base.spin(velocity=100, angle=90)
        print("spin 90 degrees")
async def main():
    machine = await connect()
    roverBase = Base.from_robot(machine, 'viam_base')
    # Move the rover in a square
    await moveInSquare(roverBase)
    await machine.close()
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 moveInSquare(ctx context.Context, base base.Base, logger logging.Logger) {
    for i := 0; i < 4; i++ {
        // moves the rover forward 600mm at 500mm/s
        base.MoveStraight(ctx, 600, 500.0, nil)
        logger.Info("move straight")
        // spins the rover 90 degrees at 100 degrees per second
        base.Spin(ctx, 90, 100.0, nil)
        logger.Info("spin 90 degrees")
    }
}
func main() {
    logger := logging.NewLogger("client")
    machine, err := client.New(
      context.Background(),
      // TODO: Replace "<MACHINE-ADDRESS>" with address from the CONNECT tab.
      "<MACHINE-ADDRESS>",
      logger,
      client.WithDialOptions(utils.WithEntityCredentials(
      // TODO: Replace "<API-KEY-ID>" (including brackets) with your machine's
      // API key ID
      "<API-KEY-ID>",
      utils.Credentials{
          Type:    utils.CredentialsTypeAPIKey,
          // TODO: Replace "<API-KEY>" (including brackets) with your machine's
          // API key
          Payload: "<API-KEY>",
      })),
    )
    if err != nil {
        logger.Fatal(err)
    }
    defer machine.Close(context.Background())
    // Get the base from the rover
    roverBase, err := base.FromRobot(machine, "viam_base")
    if err != nil {
        logger.Fatalf("cannot get base: %v", err)
    }
    // Move the rover in a square
    moveInSquare(context.Background(), roverBase, logger)
}
{
  "name": "test-rover",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "esbuild ./main.ts --bundle --outfile=static/main.js --servedir=static",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Viam Docs Team",
  "license": "ISC",
  "devDependencies": {
    "esbuild": "*"
  },
  "dependencies": {
    "@viamrobotics/sdk": "*"
  }
}
<!doctype html>
<html>
  <head>
    <title>Drive a Rover</title>
    <link rel="icon" href="favicon.ico" />
  </head>
  <body>
    <div id="main">
      <h1>Drive a rover in a square</h1>
      <p>
        We recommend you open the developer tools in your browser to see logs.
      </p>
      <p>
        Also open a second window and navigate to your rover's
        <b>CONTROL</b> tab, which allows you to interact with your rover's
        resources. Click on one of the camera panels and toggle the camera
        stream on so you can observe the rover's movements.
      </p>
      <button id="main-button" disabled="true">
        Click me to drive rover in square
      </button>
    </div>
    <script type="module" src="main.js"></script>
  </body>
</html>
// This code must be run in a browser environment.
import * as VIAM from "@viamrobotics/sdk";
// This function moves a base component in a square.
async function moveInSquare(client: VIAM.RobotClient) {
  // TODO: Replace with the name of the base on your machine.
  const name = "viam_base";
  const baseClient = new VIAM.BaseClient(client, name);
  try {
    button().disabled = true;
    for (let i = 0; i < 4; i++) {
      console.log("move straight");
      await baseClient.moveStraight(500, 500);
      console.log("spin 90 degrees");
      await baseClient.spin(90, 100);
    }
  } finally {
    button().disabled = false;
  }
}
// This function gets the button element
function button() {
  return <HTMLButtonElement>document.getElementById("main-button");
}
const main = async () => {
  // TODO: Replace "<MACHINE-ADDRESS>" with address from the CONNECT tab.
  const host = "<MACHINE-ADDRESS>";
  const machine = await VIAM.createRobotClient({
    host,
    credentials: {
      // TODO: Replace "<API-KEY>" (including brackets) with your machine's
      // API key
      type: "api-key",
      payload: "<API-KEY>",
      // TODO: Replace "<API-KEY-ID>" (including brackets) with your machine's
      // API key ID
      authEntity: "<API-KEY-ID>",
    },
    signalingAddress: "https://app.viam.com:443",
  });
  button().onclick = async () => {
    await moveInSquare(machine);
  };
  button().disabled = false;
};
main().catch((error) => {
  console.error("encountered an error:", error);
});
/// This is the BaseScreen, which allows us to control a Base.
import 'package:flutter/material.dart';
import 'package:viam_sdk/viam_sdk.dart';
import 'package:viam_sdk/widgets.dart';
class BaseScreen extends StatelessWidget {
  final Base base;
  const BaseScreen(this.base, {super.key});
  Future<void> moveSquare() async {
    for (var i=0; i<4; i++) {
      await base.moveStraight(500, 500);
      await base.spin(90, 100);
    }
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(base.name)),
      body: Center(
        child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ElevatedButton(
              onPressed: moveSquare,
              child: const Text('Move Base in Square'),
            ),
        ]))
        ,);}}
/// This is the screen that shows the resources available on a robot (or smart machine).
/// It takes in a Viam app client instance, as well as a robot client.
/// It then uses the Viam client instance to create a connection to that robot client.
/// Once the connection is established, you can view the resources available
/// and send commands to them.
import 'package:flutter/material.dart';
import 'base_screen.dart';
import 'package:viam_sdk/protos/app/app.dart';
import 'package:viam_sdk/viam_sdk.dart';
class RobotScreen extends StatefulWidget {
  final Viam _viam;
  final Robot robot;
  const RobotScreen(this._viam, this.robot, {super.key});
  @override
  State<RobotScreen> createState() => _RobotScreenState();
}
class _RobotScreenState extends State<RobotScreen> {
  /// Similar to previous screens, start with [_isLoading] to true.
  bool _isLoading = true;
  /// This is the [RobotClient], which allows you to access
  /// all the resources of a Viam Smart Machine.
  /// This differs from the [Robot] provided to us in the widget constructor
  /// in that the [RobotClient] contains a direct connection to the Smart Machine
  /// and its resources. The [Robot] object simply contains information about
  /// the Smart Machine, but is not actually connected to the machine itself.
  ///
  /// This is initialized late because it requires an asynchronous
  /// network call to establish the connection.
  late RobotClient client;
  @override
  void initState() {
    super.initState();
    // Call our own _initState method to initialize our state.
    _initState();
  }
  @override
  void dispose() {
    // You should always close the [RobotClient] to free up resources.
    // Calling [RobotClient.close] will clean up any tasks and
    // resources created by Viam.
    client.close();
    super.dispose();
  }
  /// This method will get called when the widget initializes its state.
  /// It exists outside the overridden [initState] function since it's async.
  Future<void> _initState() async {
    // Using the authenticated [Viam] the received as a parameter,
    // the app can obtain a connection to the Robot.
    // There is a helpful convenience method on the [Viam] instance for this.
    final robotClient = await widget._viam.getRobotClient(widget.robot);
    setState(() {
      client = robotClient;
      _isLoading = false;
    });
  }
  /// A computed variable that returns the available [ResourceName]s of
  /// this robot in an alphabetically sorted list.
  List<ResourceName> get _sortedResourceNames {
    return client.resourceNames..sort((a, b) => a.name.compareTo(b.name));
  }
  bool _isNavigable(ResourceName rn) {
    if (rn.subtype == Base.subtype.resourceSubtype) {
      return true;
    }
    return false;
  }
  void _navigate(ResourceName rn) {
    if (rn.subtype == Base.subtype.resourceSubtype) {
      final base = Base.fromRobot(client, rn.name);
      Navigator.of(context).push(MaterialPageRoute(builder: (_) => BaseScreen(base)));
    }
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.robot.name)),
        body: _isLoading
            ? const Center(child: CircularProgressIndicator.adaptive())
            : ListView.builder(
                itemCount: client.resourceNames.length,
                itemBuilder: (_, index) {
                  final resourceName = _sortedResourceNames[index];
                  return ListTile(
                    title: Text(resourceName.name),
                    subtitle: Text(
                        '${resourceName.namespace}:${resourceName.type}:${resourceName.subtype}'),
                    onTap: () => _navigate(resourceName),
                    trailing: _isNavigable(resourceName) ? Icon(Icons.chevron_right) : SizedBox.shrink(),
                  );
                }));
  }
}
#include <boost/optional.hpp>
#include <string>
#include <vector>
#include <viam/sdk/robot/client.hpp>
#include <viam/sdk/components/motor.hpp>
#include <viam/sdk/components/base.hpp>
#include <viam/sdk/components/camera.hpp>
#include <viam/sdk/components/encoder.hpp>
using namespace viam::sdk;
using namespace viam::sdk;
using std::cerr;
using std::cout;
using std::endl;
void move_in_square(std::shared_ptr<viam::sdk::Base> base) {
  for (int i = 0; i < 4; ++i) {
    cout << "Move straight" << endl;
    // Move the base forward 600mm at 500mm/s
    base->move_straight(500, 500);
    cout << "Spin" << endl;
    // Spin the base by 90 degree at 100 degrees per second
    base->spin(90, 100);
  }
}
int main() {
    // TODO: Replace "<MACHINE-ADDRESS>" with address from the CONNECT tab.
    std::string host("<MACHINE-ADDRESS>");
    DialOptions dial_opts;
    // TODO: Replace "<API-KEY-ID>" with your machine's API key ID
    dial_opts.set_entity(std::string("<API-KEY-ID>"));
    // TODO: Replace "<API-KEY>" with your machine's API key
    Credentials credentials("api-key", "<API-KEY>");
    dial_opts.set_credentials(credentials);
    boost::optional<DialOptions> opts(dial_opts);
    Options options(0, opts);
    auto machine = RobotClient::at_address(host, options);
    std::cout << "Resources:\n";
    for (const Name& resource : machine->resource_names()) {
      std::cout << "\t" << resource << "\n";
    }
    std::string base_name("viam_base");
    cout << "Getting base: " << base_name << endl;
    std::shared_ptr<Base> base;
    try {
        base = machine->resource_by_name<Base>(base_name);
        move_in_square(base);
    } catch (const std::exception& e) {
        cerr << "Failed to find " << base_name << ". Exiting." << endl;
        throw;
    }
    return EXIT_SUCCESS;
}
Was this page helpful?
Glad to hear it! If you have any other feedback please let us know:
We're sorry about that. To help us improve, please tell us what we can do better:
Thank you!

