Control an Intermode Rover with CAN Bus and Viam

Integrate an Intermode rover as a modular-resource-based component with CAN bus.

The Viam platform comes with a component called base, which adds useful abstractions for simplified control of mobile robots. Instead of controlling individual motors, the base component allows you to issue commands like “move_straight”, “spin”, “set_velocity” and “stop”.

Many robotic rovers can be controlled out-of-the-box with the Viam “wheeled” base model - simply by specifying how your motorized wheels are configured. But what if you want to control a rover or other mobile robot that does not expose direct motor control? This tutorial shows you how to create a modular resource (custom component). Creating a modular resouce for your robot allows you to issue commands using the same interface as you would with native Viam components. Once you have created the custom component, you can control both the Viam components and the modular resources using the Viam SDK of your choice.

Intermode rover pictured outdoors.

While the concepts covered here are applicable to other hardware, we’ll specifically show you an example of how you can get started using Viam to control the Intermode rover. This is a powerful pairing: Intermode aims to make the hardware aspects of a mobile-robot-based business simple and worry-free, while Viam simplifies the software aspects of any robotics business.

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

Hardware requirements

The tutorial uses the following hardware:

Initial Setup

Raspberry Pi software setup

Before proceeding, set up viam-server on your Raspberry Pi and configure a (for now) empty robot configuration.

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 as per the instructions.

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 will 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 will provide power to your Pi and allow it to communicate with the rover via CAN bus!

Intermode, Pi Wiring.

Software for the Intermode base modular resource

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

A modular resource for the Intermode base

Viam includes APIs for common component types within viam-server. The Viam component that exposes the interfaces for controlling a mobile robot’s movements is the base component.

If you want to learn how to leverage this API to create a custom modular resource using code found in the tutorial repository, continue reading. 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.

Both the API and model of any Viam resource are represented as colon-separated triplets where the first element is a namespace. Since you will conform to an existing Viam API for base, the API you will use is: rdk:component:base

This base model is being created for tutorial purposes only, and will implement only partial functionality for demonstration purposes. Therefore, use the namespace “viamlabs”, an (arbitrary) model family called “tutorial” and lastly, a model name of “intermode”. So the complete triplet is: viamlabs:tutorial:intermode

The module.go code creates this model and registers the component instance. The Subtype of a resource contains its API triplet, so using base.Subtype (see line 30 below) registers our new model with the API from the RDK’s built-in base component (rdk:component:base).

// namespace, model family, model
var model = resource.NewModel("viamlabs", "tutorial", "intermode")

func main() {
    goutils.ContextualMain(mainWithArgs, golog.NewDevelopmentLogger("intermodeBaseModule"))
}

func mainWithArgs(ctx context.Context, args []string, logger golog.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 golog.Logger,
        ) (interface{}, error) {
            return newBase(config.Name, logger) // note: newBase() is not shown in this tutorial
        }})
}

Implement base methods

Now that the modular resource code has registered the API it is using and its custom model, you can implement any number of 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. 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 golog.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],
    })
}

Now the intermode base can receive and execute SetPower commands using the same interface you’d use to send SetPower commands to any rover that is Viam-controlled.

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, you will leave the IsMoving method unimplemented (for illustrative purposes).

func (base *interModeBase) IsMoving(ctx context.Context) (bool, error) {
    return false, utils.NewUnimplementedInterfaceError((*interModeBase)(nil), "intermodeBase does not yet support IsMoving()")
}

Use the Intermode base modular resource

Install the modular resource

This tutorial’s modular resource code leverages libraries (specifically a CAN bus library) that can run on Linux and interface with the PiCAN socket. Once you have compiled your resouce, you need to configure viam-server (running on the Pi) to load the module. To be able to run the modular resource code from the Pi, make the modular resource code available on your Raspberry Pi. If you have git installed on your Pi, this is as simple as running the following command in the directory for your modular resource code:

git clone https://github.com/viam-labs/tutorial-intermode

If you don’t have git installed on your Pi, you’ll need to first run:

sudo apt install git

Configure the Intermode base resource

If you have not already, first create a new robot in the Viam app and follow the instructions in the SETUP tab to connect the robot to the cloud.

In order to drive the Intermode base with Viam, you need to add it to the robot configuration. You will specify where viam-server can find the module, and then configure a modular component instance for the Intermode base.

In this example, we’ve cloned the git tutorial repo to /home/me/tutorial-intermode/. Change this to the correct location in executable_path when adding the module to your robot configuration.

{
  "modules": [
    {
      "name": "intermode-base",
      "executable_path": "/home/me/tutorial-intermode/intermode-base/run.sh"
    }
  ],
    "components": [
        {
        "type": "component",
        "name": "base",
        "model": "viamlabs:tutorial:intermode",
        "namespace": "rdk",
        "attributes": {},
        "depends_on": []
        }
    ]
}

More details about modules and how they work can be found in the modular resources documentation.

Control the rover

Once you save this configuration, you see a base card in the robot’s CONTROL tab and can drive the rover from there. Be careful, the Intermode is a large and powerful rover - make sure you have the shutoff key in hand for emergencies and make sure you have enough space for the rover to move.

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

If you have any issues or if you want to connect with other developers learning how to build robots with Viam, head over to the Viam Community Slack.