Create a Modular Resource to Control a Rover like Intermode

Intermode rover pictured outdoors.

Viam supports most rovers with builtin models. If your rover is not supported out of the box, this tutorial shows you how to add support for your rover or mobile robot.

To use a rover with the Viam platform, you have to configure the rover’s components. One of the components you need to configure is called a base, which allows you to control a mobile robot using commands like “move_straight”, “spin”, “set_velocity” and “stop”. You can think of the base component as an abstraction that coordinates the movement of the motors of your base for you so you can control the higher level object as a base.

For many robotic rovers you can use the wheeled base model. Once you specify the circumference of the wheels and how far they are apart, you can then control your rover with the base component.

However, some rovers or other mobile robots do not expose direct motor control. For these types of machines, this tutorial shows you how to create a modular resource. Creating a modular resource for your robot allows you to issue commands using the same base interface as you would with builtin Viam components. Once you have created the custom component, you can control both the Viam components and the modular resources using any of the Viam SDKs. Even if your modular resource is built in Golang, you can use the Python, C++, or any other Viam SDK to issue commands.

Intermode aims to make the hardware aspects of a mobile-robot-based business simple and worry-free. This is a powerful pairing, since Viam simplifies the software aspects to revolutionize hardware.

The Intermode rover uses the CAN bus protocol, a robust and prevalent vehicle communication standard used in most modern vehicles. This tutorial shows you how to leverage this protocol and abstract it into the Viam base interface, so that your rover can be controlled securely from anywhere with the programming language of your choice.

Hardware requirements

The tutorial uses the following hardware:

Setup

Machine setup

Before you can use Viam on your device, you must ensure it has a supported operating system. If you are using a Raspberry Pi, start by setting up your Raspberry Pi. For other single-board computers, see the installation guide.

Add a new machine in the Viam app. Then follow the setup instructions to install viam-server on the computer you’re using for your project and connect to the Viam app. Wait until your machine has successfully connected.

Next, install the PiCAN 2 driver software following these instructions.

Hardware

Power your Raspberry Pi off and attach the PiCAN 2 by aligning the 40 way connector and fitting it to the top of the Pi using a spacer and a screw.

PiCAN Terminal Wiring.

Next, with the Intermode rover powered down, connect the 6-wire amphenol connector that comes with the rover to the 4 screw terminal on PiCAN bus:

  • Connect one of the 12V wires (red) to the +12V terminal
  • Connect one of the ground wires (black) to the GND terminal
  • Connect the CAN low wire (blue) to the CAN_L terminal
  • Connect the CAN high wire (white) to the CAN_H terminal.

You have two remaining wires (12V and ground).

Connect the remaining two wires to the + (red) and - (black) input terminals on your buck converter. Attach the USB-C adapter wires to the output of your buck converter, and plug the other end of the USB-C adapter into your Pi. You can now power up the rover, which also provides power to your Pi and allows it to communicate with the rover using CAN bus!

Intermode, Pi Wiring.

A modular resource for the Intermode base

The Viam platform provides APIs for common component types within viam-server. For controlling a mobile robot’s movements, the base component exposes a useful interface.

In the rest of this tutorial, you’ll learn how to use this API to create your own custom modular resource. If you want to directly configure this modular resource code with your robot, skip to using the intermode base resource

Create a custom model using the Viam RDK base API

The base component exposes an API for controlling a mobile robot’s movements. To use it for the Intermode rover, you must create a new model with its own implementation of each method.

Generally modular resources are made up of two parts:

  • The first part is the entry point for the module. It creates the model and registers it with viam-server which makes it usable by the Viam SDKs.
  • The second part implements the functionality for the API.

The full code for the modular resource is available on GitHub. This is the code for the entry point:

// namespace, repo-name, model
var model = resource.NewModel("viamlabs", "tutorial", "intermode")

func main() {
    goutils.ContextualMain(mainWithArgs, logging.NewLogger("intermodeBaseModule"))
}

func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) (err error) {
    registerBase()
    modalModule, err := module.NewModuleFromArgs(ctx, logger)

    if err != nil {
        return err
    }
    modalModule.AddModelFromRegistry(ctx, base.Subtype, model)

    err = modalModule.Start(ctx)
    defer modalModule.Close(ctx)

    if err != nil {
        return err
    }
    <-ctx.Done()
    return nil
}

// helper function to add the base's constructor and metadata to the component registry, so that we can later construct it.
func registerBase() {
    registry.RegisterComponent(
        base.Subtype, // the "base" API: "rdk:component:base"
        model,
        registry.Component{Constructor: func(
            ctx context.Context,
            deps registry.Dependencies,
            config config.Component,
            logger logging.Logger,
        ) (interface{}, error) {
            return newBase(config.Name, logger) // note: newBase() is not shown in this tutorial
        }})
}

To support this new model of a base that you are creating, you need to give the model a name. This tutorial uses the namespace viamlabs, an (arbitrary) repo-name called tutorial and lastly, the model name intermode. The complete triplet is: viamlabs:tutorial:intermode.

The entry point code defines the model name and then registers it with viam-server. When registering it, the code also provides the API that the new model supports. That means in this case that the base should support the default base API with methods such as MoveStraight and Spin.

The API of any Viam resource is also represented as colon-separated triplets where the first element is a namespace. Since you are using the default Viam API for a base, the API you are using is: rdk:component:base. In the code this is specified on line 30 as base.Subtype.

Implement base methods

Now that the modular resource code has registered the API it is using and its custom model, you can implement the methods provided by the base API. Since the Intermode rover’s commands are in the CAN bus format, you need the modular resource code to translate any commands sent from the base API, like SetPower, SetVelocity, or Stop to CAN bus frames. For reference, Intermode provides documentation on how its CAN frames are formatted.

At a high level, the tutorial code does the following:

  1. The SetPower command implements the SetPower interface from the rdk:component:base API
  2. The parameters sent to SetPower are formatted as a driveCommand
  3. The driveCommand is converted to a CAN frame, and set as the next command
  4. publishThread runs a loop continuously, sending the current command every 10ms (the Intermode base will otherwise time out)
// this struct describes intermode base drive commands
type driveCommand struct {
    Accelerator   float64
    Brake         float64
    SteeringAngle float64
    Gear          byte
    SteerMode     byte
}

func (base *interModeBase) setNextCommand(ctx context.Context, cmd modalCommand) error {
    if err := ctx.Err(); err != nil {
        return err
    }
    select {
    case <-ctx.Done():
        return ctx.Err()
    case base.nextCommandCh <- cmd.toFrame(base.logger):
    }
    return nil
}

// toFrame convert the drive command to a CANBUS data frame.
func (cmd *driveCommand) toFrame(logger logging.Logger) canbus.Frame {
    frame := canbus.Frame{
        ID:   driveId,
        Data: make([]byte, 0, 8),
        Kind: canbus.SFF,
    }
    frame.Data = append(frame.Data, calculateAccelAndBrakeBytes(cmd.Accelerator)...)
    frame.Data = append(frame.Data, calculateSteeringAngleBytes(cmd.SteeringAngle)...)

    if cmd.Accelerator < 0 {
        cmd.Gear = gears[gearReverse]
    }
    frame.Data = append(frame.Data, cmd.Gear, cmd.SteerMode)

    logger.Debugw("frame", "data", frame.Data)

    return frame
}

func (base *interModeBase) SetPower(ctx context.Context, linear, angular r3.Vector, extra map[string]interface{}) error {
    return base.setNextCommand(ctx, &driveCommand{
        Accelerator:   linear.Y * 100,  // the base API provides linear.Y between -1 (full reverse) and 1 (full forward)
        Brake:         0,
        SteeringAngle: angular.Z * 100, // the base API provides angular.Z between -1 (full left) and 1 (full right)
        Gear:          gears[gearDrive],
        SteerMode:     steerModes[steerModeFourWheelDrive],
    })
}

With this code, the intermode base can receive and execute SetPower commands using any Viam SDK.

Leaving some methods unimplemented

In some cases, you may not want to implement specific methods provided by the resource type’s API. For example, some hardware may not support specific functionality. When you want to leave a method unimplemented you must still create that method, but return an appropriate error message.

In this tutorial, the code leaves the IsMoving method unimplemented (for illustrative purposes).

func (base *interModeBase) IsMoving(ctx context.Context) (bool, error) {
    return false, errors.New("IsMoving(): unimplemented")
}

Use the Intermode base modular resource

Copy the modular resource binary

This tutorial’s modular resource code leverages libraries (specifically a CAN bus library) that run on Linux and interface with the PiCAN socket on your Raspberry Pi. The tutorial repository includes a compiled binary that is ready to run on 64-bit Raspberry Pi OS. If you make changes to the tutorial code, you’ll need to re-compile to create a new binary.

To run the modular resource, first copy the module binary to your Raspberry Pi.

Configure the Intermode base resource

You will now configure your base in the Viam app. Go to the machine you added during setup. To make your module accessible to viam-server, you must add it as a local module.

  1. Navigate to the CONFIGURE tab of your machine’s page in the Viam app.
  2. Click the + (Create) icon next to your machine part in the left-hand menu and select Local module, then Local module.
  3. Enter a Name for this instance of your modular resource, for example my-custom-base-module.
  4. Enter the module’s executable path. This path must be the absolute path to the executable on your machine’s filesystem. Add the path to where you downloaded the compiled binary.
  5. Then, click the Create button, and click Save in the upper right corner to save your config.

Now that viam-server can find the module, you can add the base component it provides for your Intermode base:

  1. On the CONFIGURE tab of your machine’s page on the Viam app:

    • Click the + (Create) icon next to your machine part in the left-hand menu and select Local module.
    • Then, select Local component.
  2. On the Create menu for a Local component:

    • Select the type of modular resource provided by your module: base, from the dropdown menu.
    • Enter the model namespace triplet of your modular resource’s model: viamlabs:tutorial:intermode.
    • Enter a name for this instance of your base, for example base-1. This name must be different from the module name.
  3. Click Create to create the modular resource provided by the local module.

  4. Click Save in the top right corner.

For more information on modules and how they work, see the modular resources documentation.

Control the rover

After you configured the base, go to the CONTROL tab and expand the base component to view the controls to enable keyboard or discrete control over your machine’s movement.

On the Keyboard tab, you can toggle the keyboard control to active. With the Keyboard toggle active, use W and S to go forward and back, and A and D to arc and spin.

Try driving your base around using the WASD keyboard controls.

If you navigate to the Discrete tab, you can use movement modes such as Straight and Spin and different movement types such as Continuous and Discrete and directions such as Forwards and Backwards.

If you do not see the base card in the CONTROL tab, check the LOGS tab for possible setup or configuration errors.

Full code for the Intermode base modular resource

Check out this GitHub repository for the working modular resource implementation example which we use in this tutorial.

Next steps

Now that you have integrated your rover or mobile base with Viam, you can use the Viam SDKs to operate your rover. If your rover has a camera or a movement_sensor, you can try the following tutorials:

You can also ask questions in the Community Discord and we will be happy to help.