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 inside the Viam app and write code to control it:
Important
If you are using your own robot for this tutorial instead of borrowing one, be sure to 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(
# 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('ADDRESS FROM THE VIAM APP', 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(),
"ADDRESS FROM THE VIAM APP",
logger,
client.WithDialOptions(utils.WithEntityCredentials(
// Replace "<API-KEY-ID>" (including brackets) with your machine's API key ID
"<API-KEY-ID>",
utils.Credentials{
Type: utils.CredentialsTypeAPIKey,
// 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 with the
<a href="https://app.viam.com">Viam app</a> 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) {
// 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 () => {
const host = "ADDRESS_FROM_VIAM_APP";
const machine = await VIAM.createRobotClient({
host,
credentials: {
// Replace "<API-KEY>" (including brackets) with your machine's api key
type: "api-key",
payload: "<API-KEY>",
// 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() {
std::string host("ADDRESS FROM THE VIAM APP");
DialOptions dial_opts;
// Replace "<API-KEY-ID>" with your machine's api key ID
dial_opts.set_entity(std::string("<API-KEY-ID>"));
// 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;
}
Next steps
Now that you have run your first code to control a machine running Viam code to control your machine, move to the next quickstart to learn how to configure and control a motor:
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!