Control a motor in 2 minutes

This quickstart is part of a series. If you haven’t read through Learn Viam and driven a rover, we recommend you do so before continuing.

In this guide you’ll configure and control a motor.

Error

Requirements

You don’t need to buy or own any hardware to complete this tutorial. If you have the following components, you can follow along on your own hardware:

  • A single-board computer or an ESP32.
  • A motor and compatible motor driver.
No motor at hand?
No problem. If you do not have both a board and motor, install viam-server on your laptop or computer and follow the instructions to use a fake motor, which is a model that serves for testing.

Instructions

Follow these steps to control your motor:

Step 1: Create a machine

Go to the Viam app. Select a location and add a new machine.

The ‘My Desk’ page on the Viam app with a new machine name in the New machine field and the Add machine button next to the field highlighted.

Step 2: Install viam-server or viam-micro-server
Navigate to the CONFIGURE tab of your machine’s page in the Viam app. Follow the setup instructions that appear on your new machine’s CONFIGURE page. If you are using a microcontroller, install viam-micro-server. Otherwise, install viam-server. Wait for your device to connect to the Viam app.
Step 3: Configure a board

On the CONFIGURE page you can add components and services to your machine. Click on the + icon to select a suitable board.

If you are using a physical board to follow along, look through the Supported Models to determine the model of component to configure. For example, configure a pi board for a Raspberry Pi 4, Raspberry Pi 3 or Raspberry Pi Zero 2 W:

An example board configuration in the app builder UI. The name (local), type (board) and model (pi) are shown. No other attributes are configured.

If you do not have a physical board, use the fake board model.

Follow the instructions in the board model’s documentation to configure any required attributes. For the fake model, there are no required attributes.

Step 4: Configure a motor

Add a motor component that supports the type of motor and motor driver you’re using. Look through the Supported Models to determine the model of component to configure. For example, if you are using a standard DC motor (brushed or brushless) wired to a typical GPIO pin-controlled motor driver, configure a gpio motor:

The CONFIGURE tab of the Viam app populated with a configured gpio motor.

Follow the motor driver manufacturer’s data sheet to wire your motor driver to your board and to your motor. Follow the model’s documentation to configure the attributes so that the computer can send signals to the motor.

If you do not have a physical motor, use the fake motor model. For the fake model, there are no required attributes.

Save your configuration.

Step 5: Choose how you will control the motor

You can control your motor directly from the Viam app, using the mobile app, or programmatically.

Option 1: Control from the app

Navigate to your machine’s CONTROL tab in the Viam app and click on the motor panel. Then use the Power % slider to set the motor’s speed. Use the Backwards and Forwards buttons to change the direction.

Option 2: Control from the mobile app

You can use the Viam mobile app to control your motor’s speed and direction directly from your smart phone.

Open the Viam mobile app and log in to your account. Select the location that your machine is in from the Locations tab.

Choose your machine from the list and use the mobile interface to adjust the motor settings.

Option 3: Control programmatically

If you have driven a rover, you have already seen that a base has a standardized API. Each component has a standardized API. The following code shows you how to control the motor’s speed and direction using the Motor API.

If you’d like to try it, find your machine’s API key and address on your machine’s CONNECT tab and run the code sample:

import asyncio
import time

from viam.robot.client import RobotClient
from viam.components.motor import Motor


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(
        '<YOUR MACHINE ADDRESS>', opts)


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

    print('Resources:')
    print(machine.resource_names)

    # Instantiate the motor client
    motor_1 = Motor.from_robot(machine, "motor-1")
    # Turn the motor at 35% power forwards
    await motor_1.set_power(power=0.35)
    # Let the motor spin for 4 seconds
    time.sleep(4)
    # Stop the motor
    await motor_1.stop()

    # Don't forget to close the machine when you're done!
    await machine.close()


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

import (
  "context"
  "time"

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

func main() {
  logger := logging.NewDebugLogger("client")
  machine, err := client.New(
    context.Background(),
    // Replace "<YOUR MACHINE ADDRESS>" (including brackets) with your machine's address
    "<YOUR MACHINE ADDRESS>",
    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())
  logger.Info("Resources:")
  logger.Info(machine.ResourceNames())


  // Instantiate the motor client
  motor1Component, err:= motor.FromRobot(machine, "motor-1")
  if err != nil {
    logger.Error(err)
    return
  }
  // Turn the motor at 35% power forwards
  err = motor1Component.SetPower(context.Background(), 0.35, nil)
  if err != nil {
    logger.Error(err)
    return
  }
  // Let the motor spin for 4 seconds
  time.Sleep(4 * time.Second)
  // Stop the motor
  err = motor1Component.Stop(context.Background(), nil)
  if err != nil {
    logger.Error(err)
    return
  }
}

Flutter code must be launched from inside a running Flutter application. To get started programming your machine with Flutter, follow the instructions to Build a Flutter App that Integrates with Viam. Then return to this page to add motor control to your app.

Add a new file to your application in /lib called motor_screen.dart. Paste this code into your file:

/// This is the MotorScreen, which allows us to control a [Motor].
/// This particular example uses both the Viam-provided [ViamMotorWidget],
/// while also providing an example of how you could create your own
/// widgets to control a resource.
library;

import 'package:flutter/material.dart';
import 'package:viam_sdk/viam_sdk.dart';
import 'package:viam_sdk/widgets.dart';

class MotorScreen extends StatelessWidget {
  final Motor motor;

  const MotorScreen(this.motor, {super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(motor.name)),
      body: Column(children: [
        // The first widget in our column will be the one provided
        // by the Viam SDK.
        ViamMotorWidget(motor: motor),
        const SizedBox(height: 10), // Padding between widgets

        // Here we have 2 buttons that control the [Motor]:
        // Either go backward or forward for 10 revolutions.
        // The [Motor] resource provides many control functions, but here
        // we are using the [Motor.goFor] method.
        //
        // You can extrapolate this to other Viam resources.
        // For example, you could make the onPressed function call
        // [Gripper.open] on a gripper, or [Sensor.readings] on a Sensor.
        Row(mainAxisAlignment: MainAxisAlignment.center, children: [
          ElevatedButton(
            onPressed: () => {motor.goFor(100, -10)},
            style: ElevatedButton.styleFrom(
            minimumSize: Size(80, 20), // Adjusts width and height of the button
            padding: EdgeInsets.symmetric(horizontal: 4, vertical: 6), // Adjusts padding inside the button
          ),
            child: const Text('Go Backwards 10 Revolutions', textAlign: TextAlign.center),
          ),
          const SizedBox(width: 16), // Padding between widgets
          ElevatedButton(
            onPressed: () => {motor.goFor(100, 10)},
            style: ElevatedButton.styleFrom(
            minimumSize: Size(80, 20), // Adjusts width and height of the button
            padding: EdgeInsets.symmetric(horizontal: 4, vertical: 6), // Adjusts padding inside the button
          ),
            child: const Text('Go Forwards 10 Revolutions', textAlign: TextAlign.center),
          ),
        ]),
      ]),
    );
  }
}

This code creates a screen with a power widget to adjust the power and two buttons that, when pressed, call on the goFor() method to make the motor either go forwards 10 revolutions or go backwards 10 revolutions.

Then, replace the contents of robot_screen.dart with the following file, or add the highlighted lines of code to your program in the locations indicated:

/// 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.
library;

import 'package:flutter/material.dart';
import 'motor_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 == Motor.subtype.resourceSubtype) {
      return true;
    }
    return false;
  }

  void _navigate(ResourceName rn) {
    if (rn.subtype == Motor.subtype.resourceSubtype) {
      final motor = Motor.fromRobot(client, rn.name);
      Navigator.of(context).push(MaterialPageRoute(builder: (_) => MotorScreen(motor)));
    }
  }

  @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(),
                  );
                }));
  }
}

This imports the motor_screen.dart file into the program and adds logic to check if a resource is “navigable”, or, has a screen made for it. Since you added a screen for motor, motor is a navigable resource.

To navigate to the motor screen, save your code and launch your simulator. Navigate to the robot screen of your (live) machine with a motor resource configured, and see the resource control interface displayed:

iOS simulator of a motor displayed

You can adjust the toggle to change the power of your motor or press the buttons to make it revolve forwards and backwards.

package.json:

{
  "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": "*"
  }
}

static/index.html:

<!doctype html>
<html>
  <head>
    <title>Drive a Rover</title>
    <link rel="icon" href="favicon.ico" />
  </head>
  <body>
    <div id="main">
      <button id="main-button" disabled="true">Click me</button>
    </div>
    <script type="module" src="main.js"></script>
  </body>
</html>

main.ts:

// This code must be run in a browser environment.

import * as VIAM from "@viamrobotics/sdk";

// 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,
    credential: {
      type: "api-key",
      // Replace "<API-KEY>" (including brackets) with your machine's 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 () => {
    // Instantiate the motor client
    // Replace with the name of a motor on your machine.
    const name = "motor-1";
    const motorClient = new VIAM.MotorClient(machine, name);

    // Turn the motor at 35% power forwards
    await motorClient.setPower(0.35);
    // Let the motor spin for 4 seconds, then stop the motor
    const sleep = (ms: number) =>
      new Promise((resolve) => setTimeout(resolve, ms));
    await sleep(4000);
    await motorClient.stop();
  };
  button().disabled = false;
};

main().catch((error) => {
  console.error("encountered an error:", error);
});
#include <boost/optional.hpp>
#include <string>
#include <vector>
#include <unistd.h>
#include <viam/sdk/robot/client.hpp>
#include <viam/sdk/components/motor.hpp>

using namespace viam::sdk;

int main() {
    std::string host("muddy-snow-main.7kp7y4p393.viam.cloud");
    DialOptions dial_opts;
    dial_opts.set_entity(std::string("<API-KEY-ID>"));
    /* Replace "<API-KEY-ID>" (including brackets) with your machine's api key id */
    Credentials credentials("api-key", "<API-KEY>");
    /* Replace "<API-KEY>" (including brackets) with your machine's 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 motor_name("motor-1");

    std::cout << "Getting motor: " << motor_name << std::endl;
    std::shared_ptr<Motor> motor;
    try {
        // Get the motor client
        motor = machine->resource_by_name<Motor>(motor_name);
        // Turn the motor at 35% power forwards
        motor->set_power(0.35);
        // Let the motor spin for 4 seconds
        sleep(3);
        // Stop the motor
        motor->stop();
    } catch (const std::exception& e) {
        std::cerr << "Failed to find " << motor_name << ". Exiting." << std::endl;
        throw;
    }
    return EXIT_SUCCESS;
}

Next steps

Now that you have configured and controlled a motor, the next quickstart will introduce you to your first service: