Create a Hello World module

This guide will walk you through creating a modular camera component that responds to API calls by returning a configured image. This guide also includes optional steps to create a modular sensor that returns random numbers, to demonstrate how you can include two modular resources within one module. By the end of this guide, you will be able to create your own modular resources and package them into modules so you can use them on your machines.

Prerequisites

Install the Viam CLI and authenticate

Install the Viam CLI and authenticate to Viam, from the same machine that you intend to upload your module from.

To download the Viam CLI on a macOS computer, install brew and run the following commands:

brew tap viamrobotics/brews
brew install viam

To download the Viam CLI on a Linux computer with the aarch64 architecture, run the following commands:

sudo curl -o /usr/local/bin/viam https://storage.googleapis.com/packages.viam.com/apps/viam-cli/viam-cli-stable-linux-arm64
sudo chmod a+rx /usr/local/bin/viam

To download the Viam CLI on a Linux computer with the amd64 (Intel x86_64) architecture, run the following commands:

sudo curl -o /usr/local/bin/viam https://storage.googleapis.com/packages.viam.com/apps/viam-cli/viam-cli-stable-linux-amd64
sudo chmod a+rx /usr/local/bin/viam

You can also install the Viam CLI using brew on Linux amd64 (Intel x86_64):

brew tap viamrobotics/brews
brew install viam

If you have Go installed, you can build the Viam CLI directly from source using the go install command:

go install go.viam.com/rdk/cli/viam@latest

To confirm viam is installed and ready to use, issue the viam command from your terminal. If you see help instructions, everything is correctly installed. If you do not see help instructions, add your local go/bin/* directory to your PATH variable. If you use bash as your shell, you can use the following command:

echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.bashrc

For more information see install the Viam CLI.

Authenticate your CLI session with Viam using one of the following options:

viam login

This will open a new browser window with a prompt to start the authentication process. If a browser window does not open, the CLI will present a URL for you to manually open in your browser. Follow the instructions to complete the authentication process.

Use your organization, location, or machine part API key and corresponding API key ID in the following command:

viam login api-key --key-id <api-key-id> --key <organization-api-key-secret>
Install viam-server on your computer and connect to the Viam app
Add a new machine in the Viam app. On the machine’s page, follow the setup instructions to install viam-server on the computer you’re using for your project. Wait until your machine has successfully connected to the Viam app.

Create a test script

The point of creating a module is to add functionality to your machine. For the purposes of this guide, you’re going to make a module that does two things: It opens an image file from a configured path on your machine, and it returns a random number.

  1. Find an image you’d like to display when your program runs. We used this image of a computer with “hello world” on the screen. Save the image to your computer.

  2. Create a test script on your computer and copy the following code into it:

    # test.py opens an image and prints a random number
    from PIL import Image
    import random
    
    # TODO: Replace path with path to where you saved your photo
    photo = Image.open("/Users/jessamyt/Downloads/hello-world.jpg")
    
    photo.show()
    
    number = random.random()
    
    print("Hello, World! The latest random number is ", number, ".")
    
    // test.go opens an image and prints a random number
    package main
    
    import (
      "os"
      "os/exec"
      "fmt"
      "math/rand"
    )
    
    func main() {
    
      // TODO: Replace path string with path to where you saved your photo
      imagePath := "/Users/jessamyt/Downloads/hello-world.jpg"
      file, err := os.Open(imagePath)
      if err != nil {
        fmt.Println("Error opening image:", err)
        return
      }
      defer file.Close()
    
      // "open" works on macOS.
      // For Linux, replace "open" with "xdg-open".
      err = exec.Command("open", imagePath).Start()
      if err != nil {
        fmt.Println("Error opening image viewer:", err)
      }
    
      number := rand.Float64()
      fmt.Println("Hello, World! The latest random number is ", number, ".")
    }
    
  3. Replace the path in the script above with the path to where you saved your photo. Save the script.

  4. Run the test script in your terminal:

    It’s best practice to use a virtual environment for running Python scripts. You’ll also need to install the dependency Pillow in the virtual environment before running the test script.

    python3 -m venv .venv
    source .venv/bin/activate
    pip install Pillow
    python3 test.py
    

    The image you saved should open on your screen, and a random number should print to your terminal.

    In later steps, the module generator will create a new virtual environment with required dependencies, so you can deactivate the one you just ran the test script in:

    deactivate
    
    go run test.go
    

    The image you saved should open on your screen, and a random number should print to your terminal.

Choose an API to implement

Now it’s time to decide which Viam APIs make sense for your module. You need a way to return an image, and you need a way to return a number.

If you look at the camera API, you can see the GetImage method, which returns an image. That will work for the image. None of the camera API methods return a number though.

Look at the sensor API, which includes the GetReadings method. You can return a number with that, but the sensor API can’t return an image.

Your module can contain multiple modular resources, so let’s make two modular resources: a camera to return the image, and a sensor to return a random number.

Generate stub files

The easiest way to generate the files for your module is to use the Viam CLI.

Generate the camera files

The CLI module generator generates the files for one modular resource at a time. First let’s generate the camera component files, and we’ll add the sensor code later.

  1. Run the module generate command in your terminal:

    viam module generate
    
  2. Follow the prompts, selecting the following options:

    • Module name: hello-world
    • Language: Your choice
    • Visibility: Private
    • Namespace/Organization ID:
      • In the Viam app, navigate to your organization settings through the menu in upper right corner of the page. Find the Public namespace and copy that string. In the example snippets below, the namespace is jessamy.
    • Resource to add to the module (API): Camera Component. We will add the sensor later.
    • Model name: hello-camera
    • Enable cloud build: No
    • Register module: Yes
  3. Hit your Enter key and the generator will generate a folder called hello-world containing stub files for your modular camera component.

Generate the sensor code

Click if you are also creating a sensor component

Some of the code you just generated is shared across the module no matter how many modular resource models it supports. Some of the code you generated is camera-specific. You need to add some sensor-specific code to support the sensor component.

  1. Instead of writing the code manually, use the module generator again.

    viam module generate
    
  2. You’re going to delete this module after copy-pasting the sensor-specific code from it. The only things that matter are the API and the model name.

    • Module name: temporary
    • Language: Your choice
    • Visibility: Private
    • Namespace/Organization ID: Same as you used before.
    • Resource to add to the module (API): Sensor Component.
    • Model name: hello-sensor
    • Enable cloud build: No
    • Register module: No
  1. Open temporary/src/main.py. Copy the sensor class definition, from class HelloSensor(Sensor, EasyResource) through the get_readings() function definition (lines 15-65).

    Open the hello-world/src/main.py file you generated earlier, and paste the sensor class definition in after the camera class definition, above if __name__ == "__main__":.

  2. Change temporary to hello-world in the ModelFamily line, so you have, for example:

    MODEL: ClassVar[Model] = Model(ModelFamily("jessamy", "hello-world"), "hello-sensor")
    
  3. Add the imports that are unique to the sensor file:

    from viam.components.sensor import *
    from viam.utils import SensorReading
    

    Save the hello-world/src/main.py file.

  1. Edit the file structure:

    1. Change the name of hello-world/models/module.go to hello-camera.go.

    2. Change the name of temporary/models/module.go to hello-sensor.go. Move the hello-sensor.go folder from temporary/models/ to /hello-world/models/.

  2. Open hello-world/main.go. You need to add the necessary imports and define how it adds the sensor model from the registry. Delete all the contents and replace them with the following:

     package main
    
     import (
         "context"
         "hello-world/models"
    
         "go.viam.com/rdk/components/camera"
         "go.viam.com/rdk/components/sensor"
         "go.viam.com/rdk/logging"
         "go.viam.com/rdk/module"
         "go.viam.com/utils"
     )
    
     func main() {
         utils.ContextualMain(mainWithArgs, module.NewLoggerFromArgs("hello-world"))
     }
    
     func mainWithArgs(ctx context.Context, args []string, logger logging.Logger) error {
         helloWorld, err := module.NewModuleFromArgs(ctx)
         if err != nil {
             return err
         }
    
         if err = helloWorld.AddModelFromRegistry(ctx, camera.API, models.HelloCamera); err != nil {
             return err
         }
    
         if err = helloWorld.AddModelFromRegistry(ctx, sensor.API, models.HelloSensor); err != nil {
             return err
         }
    
         err = helloWorld.Start(ctx)
         defer helloWorld.Close(ctx)
         if err != nil {
             return err
         }
    
         <-ctx.Done()
         return nil
     }
    

    Save the file.

  3. Change all instances of temporary in hello-world/models/hello-sensor.go:

    1. Edit temporary to hello-world on line 14, so it looks like this (but with your org ID in place of jessamy):

      HelloSensor      = resource.NewModel("jessamy", "hello-world", "hello-sensor")
      
    2. Change both instances of newTemporaryHelloSensor to newHelloWorldHelloSensor.

    3. Search for any other instances of temporary in the rest of the hello-world/models/hello-sensor.go and replace each with helloWorld.

  1. Open temporary/meta.json and copy the model information. For example:

    {
      "api": "rdk:component:sensor",
      "model": "jessamy:temporary:hello-sensor"
    }
    
  2. Open hello-world/meta.json and paste the sensor model into the model list.

    Edit the description to include both models.

    Change temporary to hello-world.

    The file should now resemble the following:

    {
      "$schema": "https://dl.viam.dev/module.schema.json",
      "module_id": "jessamy:hello-world",
      "visibility": "private",
      "url": "",
      "description": "Example camera and sensor components: hello-camera and hello-sensor",
      "models": [
        {
          "api": "rdk:component:camera",
          "model": "jessamy:hello-world:hello-camera"
        },
        {
          "api": "rdk:component:sensor",
          "model": "jessamy:hello-world:hello-sensor"
        }
      ],
      "entrypoint": "./run.sh",
      "first_run": ""
    }
    
  3. You can now delete the temporary module directory and all its contents.

Implement the API methods

Edit the stub files to add the logic from your test script in a way that works with the camera and sensor APIs:

Implement the camera API

First, implement the camera API methods by editing the camera class definition:

  1. Add the following to the list of imports at the top of hello-world/src/main.py:

    from viam.media.utils.pil import pil_to_viam_image
    from viam.media.video import CameraMimeType
    from viam.utils import struct_to_dict
    from PIL import Image
    
  2. In the test script you hard-coded the path to the image. For the module, let’s make the path a configurable attribute so you or other users of the module can set the path from which to get the image. Add the following lines to the camera’s reconfigure() function definition. These lines set the image_path based on the configuration when the resource is configured or reconfigured.

    attrs = struct_to_dict(config.attributes)
    self.image_path = str(attrs.get("image_path"))
    
  3. We are not providing a default image but rely on the end user to supply a valid path to an image when configuring the resource. This means image_path is a required attribute. Add the following code to the validate() function to throw an error if image_path isn’t configured:

    # Check that a path to get an image was configured
    fields = config.attributes.fields
    if not "image_path" in fields:
        raise Exception("Missing image_path attribute.")
    elif not fields["image_path"].HasField("string_value"):
        raise Exception("image_path must be a string.")
    
  4. The module generator created a stub for the get_image() function we want to implement:

     async def get_image(
         self,
         mime_type: str = "",
         *,
         extra: Optional[Dict[str, Any]] = None,
         timeout: Optional[float] = None,
         **kwargs
     ) -> ViamImage:
         raise NotImplementedError()
    

    You need to replace raise NotImplementedError() with code to actually implement the method:

    ) -> ViamImage:
        img = Image.open(self.image_path)
        return pil_to_viam_image(img, CameraMimeType.JPEG)
    

    You can leave the rest of the functions not implemented, because this module is not meant to return a point cloud (get_point_cloud()), and does not need to return multiple images simultaneously (get_images()).

    Save the file.

  5. Open requirements.txt. Add the following line:

    Pillow
    

Implement the sensor API

Click if you are also creating a sensor component

Now edit the sensor class definition to implement the sensor API. You don’t need to edit any of the validate or configuration methods because you’re not adding any configurable attributes for the sensor model.

  1. Add random to the list of imports in main.py for the random number generation:

    import random
    
  2. The sensor API only has one resource-specific method, get_readings():

     async def get_readings(
         self,
         *,
         extra: Optional[Mapping[str, Any]] = None,
         timeout: Optional[float] = None,
         **kwargs
     ) -> Mapping[str, SensorReading]:
         raise NotImplementedError()
    

    Replace raise NotImplementedError() with the following code:

     ) -> Mapping[str, SensorReading]:
         number = random.random()
         return {
             "random_number": number
         }
    

    Save the file.

Implement the camera API

First, implement the camera API methods by editing the camera class definition:

  1. Add the following to the list of imports at the top of hello-world/models/hello-camera.go:

    "os"
    "reflect"
    "io/ioutil"
    
  2. Add imagePath = "" to the global variables so you have the following:

    var (
        HelloCamera      = resource.NewModel("jessamy", "hello-world", "hello-camera")
        errUnimplemented = errors.New("unimplemented")
        imagePath        = ""
    )
    
  3. In the test script you hard-coded the path to the image. For the module, let’s make the path a configurable attribute so you or other users of the module can set the path from which to get the image.

    Edit the type Config struct definition, replacing the comments with the following:

    type Config struct {
        resource.AlwaysRebuild
        ImagePath string `json:"image_path"`
    }
    

    This adds the image_path attribute and causes the resource to rebuild each time the configuration is changed.

  4. We are not providing a default image but rely on the end user to supply a valid path to an image when configuring the resource. This means image_path is a required attribute. Replace the Validate function with the following code to throw an error if image_path isn’t configured or isn’t a string:

    func (cfg *Config) Validate(path string) ([]string, error) {
      var deps []string
      if cfg.ImagePath == "" {
          return nil, resource.NewConfigValidationFieldRequiredError(path, "image_path")
      }
      if reflect.TypeOf(cfg.ImagePath).Kind() != reflect.String {
          return nil, errors.New("image_path must be a string.")
      }
      imagePath = cfg.ImagePath
      return deps, nil
    }
    
  5. The module generator created a stub for the Image function we want to implement:

    func (s *helloWorldHelloCamera) Image(ctx context.Context, mimeType string, extra map[string]interface{}) ([]byte, camera.ImageMetadata, error) {
        panic("not implemented")
    }
    

    You need to replace panic("not implemented") with code to actually implement the method:

    imgFile, err := os.Open(imagePath)
    if err != nil {
      return nil, camera.ImageMetadata{}, errors.New("Error opening image.")
    }
    defer imgFile.Close()
    imgByte, err := ioutil.ReadFile(imagePath)
    return imgByte, camera.ImageMetadata{}, nil
    
  6. Delete the SubscribeRTP and Unsubscribe methods, since they are not applicable to this camera.

  7. You can leave the rest of the functions not implemented, because this module is not meant to return a point cloud (NextPointCloud), and does not need to return multiple images simultaneously (Images). If this camera returned a camera stream instead of a single static file, we would have implemented Stream instead of Read.

    However, you do need to edit the return statements to return empty structs that match the API. Edit these methods so they look like this:

    func (s *helloWorldHelloCamera) NewClientFromConn(ctx context.Context, conn rpc.ClientConn, remoteName string, name resource.Name, logger logging.Logger) (camera.Camera, error) {
        return nil, errors.New("not implemented")
    }
    
    func (s *helloWorldHelloCamera) Stream(ctx context.Context, errHandlers ...gostream.ErrorHandler) (gostream.VideoStream, error) {
        return nil, errors.New("not implemented")
    }
    
    func (s *helloWorldHelloCamera) Images(ctx context.Context) ([]camera.NamedImage, resource.ResponseMetadata, error) {
        return []camera.NamedImage{}, resource.ResponseMetadata{}, errors.New("not implemented")
    }
    
    func (s *helloWorldHelloCamera) NextPointCloud(ctx context.Context) (pointcloud.PointCloud, error) {
        return nil, errors.New("not implemented")
    }
    
    func (s *helloWorldHelloCamera) Properties(ctx context.Context) (camera.Properties, error) {
        return camera.Properties{}, errors.New("not implemented")
    }
    
    func (s *helloWorldHelloCamera) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) {
        return map[string]interface{}{}, errors.New("not implemented")
    }
    
  8. Save the file.

Implement the sensor API

Click if you are also creating a sensor component

Now edit the sensor class definition to implement the sensor API. You don’t need to edit any of the validate or configuration methods because you’re not adding any configurable attributes for the sensor model.

  1. Add "math/rand" to the list of imports in hello-sensor.go for the random number generation.

  2. Since errUnimplemented and Config are defined in hello-camera.go, you need to change hello-sensor.go to avoid redeclaring them:

    • Delete line 16, errUnimplemented = errors.New("unimplemented") from hello-sensor.go.

    • On line 27, change type Config struct { to type sensorConfig struct {.

    • Search for all instances of *Config in hello-sensor.go and change them to *sensorConfig.

  3. The sensor API only has one resource-specific method, Readings:

    func (s *helloWorldHelloSensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) {
        panic("not implemented")
    }
    

    Replace panic("not implemented") with the following code:

    number := rand.Float64()
    return map[string]interface{}{
       "random_number": number,
    }, nil
    
  4. In the NewClientFromConn definition, replace panic("not implemented") with the following:

    return nil, errUnimplemented
    
  5. In the DoCommand definition, replace panic("not implemented") with the following:

    return map[string]interface{}{}, errors.New("not implemented")
    
  6. Save the file.

Test your module

With the implementation written, it’s time to test your module locally:

  1. Create a virtual Python environment with the necessary packages by running the setup file from within the hello-world directory:

    sh setup.sh
    

    This environment is where the local module will run. viam-server does not need to run inside this environment.

  1. From within the hello-world directory, compile your module into a single executable:

    make setup
    make build
    
  1. Make sure your machine’s instance of viam-server is live and connected to the Viam app.

  2. In the Viam app, navigate to your machine’s CONFIGURE page.

  3. Click the + button, select Local module, then again select Local module.

  4. Enter the path to the automatically-generated run.sh file, for example, /Users/jessamyt/myCode/hello-world/run.sh. Click Create.

  5. Now add the modular camera resource provided by the module:

    Click +, click Local module, then click Local component.

    For the model namespace triplet, select or enter <namespace>:hello-world:hello-camera, replacing <namespace> with the organization namespace you used when generating the stub files. For example, jessamy:hello-world:hello-camera.

    For type, enter camera.

    For name, you can use the automatic camera-1.

  6. Configure the image path attribute by pasting the following in place of the {} brackets:

    {
      "image_path": "<replace with the path to your image>"
    }
    

    Replace the path with the path to your image, for example "/Users/jessamyt/Downloads/hello-world.jpg".

  7. Save the config, then click the TEST section of the camera’s configuration card.

    The Viam app configuration interface with the Test section of the camera card open, showing a hello world image.

    You should see your image displayed. If not, check the LOGS tab for errors.

Click if you also created a sensor component
  1. Add the modular sensor:

    Click +, click Local module, then click Local component.

    For the model namespace triplet, select or enter <namespace>:hello-world:hello-sensor, replacing <namespace> with the organization namespace you used when generating the stub files. For example, jessamy:hello-world:hello-sensor.

    For type, enter sensor.

    For name, you can use the automatic sensor-1.

  2. Save the config, then click TEST to see a random number generated every second.

    The sensor card test section open.

Package and upload the module

You now have a working local module. To make it available to deploy on more machines, you can package it and upload it to the Viam Registry.

The hello world module you created is for learning purposes, not to provide any meaningful utility, so we recommend making it available only to machines within your organization instead of making it publicly available.

Click to see what you would do differently if this wasn't just a hello world module
  1. Create a GitHub repo with all the source code for your module. Add the link to that repo as the url in the meta.json file.
  2. Create a README to document what your module does and how to configure it.
  3. If you wanted to share the module outside of your organization, you’d set "visibility": "public" in the meta.json file.

To package (for Python) and upload your module and make it available to configure on machines in your organization:

  1. To package the module as an archive, run the following command from inside the hello-world directory:

    tar -czf module.tar.gz run.sh setup.sh requirements.txt src
    

    This creates a tarball called module.tar.gz.

  2. Run the viam module upload CLI command to upload the module to the registry:

    viam module upload --version 1.0.0 --platform any module.tar.gz
    

From within your hello-world directory, run the viam module upload CLI command to upload the module to the registry:

viam module upload --version 1.0.0 --platform any .

Now, if you look at the Viam Registry page while logged into your account, you’ll be able to find your private module listed. Because the module is now in the registry, you can configure the hello-sensor and hello-camera on your machines just as you would configure other components and services; there’s no more need for local module configuration. The local module configuration is primarily for testing purposes.

The create a component menu open, searching for hello. The hello-camera and hello-sensor components are shown in the search results.

For more information about uploading modules, see Upload a module.

Next steps

For a guide that walks you through creating different sensor models, for example to get weather data from an online source, see Create a sensor module with Python.

For more module creation information with more programming language options, see the Create a module guide.

To update or delete a module, see Update and manage modules.

Have questions, or want to meet other people working on robots? Join our Community Discord.

If you notice any issues with the documentation, feel free to file an issue or edit this file.