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
Renting a Viam Rover?
See Rent a Viam Rover for reservation steps, session time limits, and answers to common rental questions.
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():
async with await connect() as machine:
roverBase = Base.from_robot(machine, 'viam_base')
# Move the rover in a square
await moveInSquare(roverBase)
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.FromProvider(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 <iostream>
#include <unistd.h>
#include <viam/sdk/common/instance.hpp>
#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 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() {
// Every Viam C++ SDK program must have one and only one Instance object which is created
// before
// any other C++ SDK objects and stays alive until all Viam C++ SDK objects are destroyed.
Instance inst;
// 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!

