Previous
Lifecycle of a module
If your physical or virtual hardware is not supported by an existing registry module, you can create a new module to add support for it.
This page provides instructions for creating a module in Python or Go. For C++ module examples, see the C++ examples directory on GitHub. If you want to create a module for use with a microcontroller, see Modules for ESP32.
Example module: With each step of this guide, you have instructions for creating a module which does two things:
While not required, we recommend starting by writing a test script to check that you can connect to and control your hardware from your computer, perhaps using the manufacturer’s API or other low-level code.
Example module: For the example module, the test script will open an image in the same directory and print a random number.
import random
from PIL import Image
# Open an image
img = Image.open("example.png")
img.show()
# Return a random number
random_number = random.random()
print(random_number)
package main
import (
  "fmt"
  "math/rand"
  "os"
)
func main() {
  // Open an image
  imgFile, err := os.Open("example.png")
  if err != nil {
    fmt.Printf("Error opening image file: %v\n", err)
    return
  }
  defer imgFile.Close()
  imgByte, err := os.ReadFile("example.png")
  fmt.Printf("Image file type: %T\n", imgByte)
  if err != nil {
    fmt.Printf("Error reading image file: %v\n", err)
    return
  }
  // Return a random number
  number := rand.Float64()
  fmt.Printf("Random number: %f\n", number)
}
You can think of a module as a packaged wrapper around a script. The module takes the functionality of the script and maps it to a standardized API for use within the Viam ecosystem.
Review the available component APIs and choose the one whose methods map most closely to the functionality you need.
If you need a method that is not in your chosen API, you can use the flexible DoCommand (which is built into all component APIs) to create custom commands.
See Run control logic for more information.
Example module: To choose the Viam APIs that make sense for your module, think about the functionality you want to implement. 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.
The camera API also has a few other methods.
You do not need to fully implement all the methods of an API.
For example, this camera does not use point cloud data, so for methods like GetPointCloud it will return an “unimplemented” error.
The sensor API includes the GetReadings method.
You can return the random number with that.
Note that the camera API can’t return a number and the sensor API can’t return an image. Each model can implement only one API, but your module can contain multiple modular resources. Therefore it is best to make two modular resources: a camera to return the image and a sensor to return a random number.
Use the Viam CLI to generate template files for your module:
Run the module generate command in your terminal:
viam module generate
Example module: To build an example module that contains a camera model, use the following command:
viam module generate --language python --model-name hello-camera \
  --name hello-world --resource-subtype=camera --public false \
  --enable-cloud true
viam module generate --language go --model-name hello-camera \
  --name hello-world --resource-subtype=camera --public false \
  --enable-cloud true
The CLI only supports generating code for one model at a time. You can add the model for the sensor in a later step in Creating multiple models within one module.
The generator creates a directory containing stub files for your modular component. In the next section, you’ll customize some of the generated files to support your camera.
Example module: For the example module, the file structure is:
hello-world/
└── src/
|   ├── models/
|   |   └── hello_camera.py
|   └── main.py
└── README.md
└── build.sh
└── meta.json
└── requirements.txt
└── run.sh
└── setup.sh
If you want to understand the module structure, here’s what each file does:
hello-world/
└── cmd/
|   ├── cli/
|   |   └── main.go
|   └── module/
|       └── main.go
└── Makefile
└── README.md
└── go.mod
└── module.go
└── meta.json
If you want to understand the module structure, here’s what each file does:
go run ./cmd/cli).Some of the code you generated for your first modular resource is shared across the module no matter how many modular resource models it supports. Some of the code you generated is resource-specific.
If you have multiple modular resources that are related, you can put them all into the same module.
For convenience, we recommend running the module generator again from within the first module’s directory, generating an unregistered module, and copying the resource-specific code from it.
Example module: Change directory into the first module’s directory:
cd hello-world
Run the following command from within the first module’s directory to generate temporary code you can copy from. Do not register this module.
viam module generate --language python --model-name hello-sensor \
  --name hello-world --resource-subtype=sensor --public false \
  --enable-cloud true
Click on each tab to see how the file should change to add the sensor-specific code:
Move the generated 
Open the HelloSensor to the list of imports so you have:
import asyncio
from viam.module.module import Module
try:
    from models.hello_camera import HelloCamera
    from models.hello_sensor import HelloSensor
except ModuleNotFoundError:  # when running as local module with run.sh
    from .models.hello_camera import HelloCamera
    from .models.hello_sensor import HelloSensor
if __name__ == '__main__':
    asyncio.run(Module.run_from_registry())
Save the file.
Open description to include both models.
{
  "$schema": "https://dl.viam.dev/module.schema.json",
  "module_id": "exampleorg:hello-world",
  "visibility": "private",
  "url": "",
  "description": "Example camera and sensor components: hello-camera and hello-sensor",
  "models": [
    {
      "api": "rdk:component:camera",
      "model": "exampleorg:hello-world:hello-camera",
      "short_description": "A camera that returns an image.",
      "markdown_link": "README.md#model-exampleorghello-worldhello-camera"
    },
    {
      "api": "rdk:component:sensor",
      "model": "exampleorg:hello-world:hello-sensor",
      "short_description": "A sensor that returns a random number.",
      "markdown_link": "README.md#model-exampleorghello-worldhello-sensor"
    }
  ],
  "applications": null,
  "markdown_link": "README.md",
  "entrypoint": "./run.sh",
  "first_run": "",
  "build": {
    "build": "./build.sh",
    "setup": "./setup.sh",
    "path": "dist/archive.tar.gz",
    "arch": ["linux/amd64", "linux/arm64", "darwin/arm64", "windows/amd64"]
  }
}
Save the file.
viam module generate --language go --model-name hello-sensor \
  --name hello-world --resource-subtype=sensor --public false \
  --enable-cloud true
Click on each tab to see how the file should change to add the sensor-specific code:
In the initial module, change the name of 
Move and rename 
Open 
package main
import (
    "helloworld"
    "go.viam.com/rdk/module"
    "go.viam.com/rdk/resource"
    camera "go.viam.com/rdk/components/camera"
    sensor "go.viam.com/rdk/components/sensor"
)
func main() {
    // ModularMain can take multiple APIModel arguments, if your module implements multiple models.
    module.ModularMain(
      resource.APIModel{ camera.API, helloworld.HelloCamera},
      resource.APIModel{ sensor.API, helloworld.HelloSensor},
    )
}
Save the file.
Open description to include both models.
{
  "$schema": "https://dl.viam.dev/module.schema.json",
  "module_id": "exampleorg:hello-world",
  "visibility": "private",
  "url": "",
  "description": "Example camera and sensor components: hello-camera and hello-sensor",
  "models": [
    {
      "api": "rdk:component:camera",
      "model": "exampleorg:hello-world:hello-camera",
      "short_description": "A camera that returns an image.",
      "markdown_link": "README.md#model-exampleorghello-worldhello-camera"
    },
    {
      "api": "rdk:component:sensor",
      "model": "exampleorg:hello-world:hello-sensor",
      "short_description": "A sensor that returns a random number.",
      "markdown_link": "README.md#model-exampleorghello-worldhello-sensor"
    }
  ],
  "applications": null,
  "markdown_link": "README.md",
  "entrypoint": "bin/hello-world",
  "first_run": "",
  "build": {
    "build": "make module.tar.gz",
    "setup": "make setup",
    "path": "module.tar.gz",
    "arch": ["linux/amd64", "linux/arm64", "darwin/arm64", "windows/amd64"]
  }
}
Save the file.
You can now delete the temporary 
At this point you have a template for your module.
If you want to see example modules, check out the Viam Registry. Many modules have a linked GitHub repo, where you can see the module’s code. When logged in, you can also download the module’s source code to inspect it.
Generally you will add your custom logic in these files:
| File | Description | 
|---|---|
| Set up the configuration options for the model and implement the API methods for the model. | |
| setup.shandrun.sh | Add any logic for installing or running other software for your module. | 
| requirements.txt | Add any Python packages that are required for your module. They will be installed by setup.sh. | 
Generally you will add your custom logic in these files:
| File | Description | 
|---|---|
| Model file (for example hello-camera.go) | Implement the API methods for the model. | 
Example module: You can view complete example code in the hello-world-module repository on GitHub.
Many resource models have configuration options that allow you to specify options such as:
Model configuration happens in two steps:
Validation
The validation step serves two purposes:
viam-server will pass these resources to the next step as dependencies.
For more information, see Module dependencies.Example module: Imagine how a user might configure the finished camera model. Since the camera model returns an image at a provided path, the configuration must contain a variable to pass in the file path.
{
  "image_path": "/path/to/file"
}
In validate_config function to:
    @classmethod
    def validate_config(
        cls, config: ComponentConfig
    ) -> Tuple[Sequence[str], Sequence[str]]:
        # Check that a path to get an image was configured
        fields = config.attributes.fields
        if "image_path" not in fields:
            raise Exception("Missing image_path attribute.")
        elif not fields["image_path"].HasField("string_value"):
            raise Exception("image_path must be a string.")
        return [], []
In Validate function to:
func (cfg *Config) Validate(path string) ([]string, []string, error) {
    var deps []string
    if cfg.ImagePath == "" {
        return nil, nil, resource.NewConfigValidationFieldRequiredError(path, "image_path")
    }
    if reflect.TypeOf(cfg.ImagePath).Kind() != reflect.String {
        return nil, nil, errors.New("image_path must be a string.")
    }
    imagePath = cfg.ImagePath
    return deps, []string{}, nil
}
Add the following import at the top of 
"reflect"
For the sensor model, you do not need to edit any of the validation or configuration methods because the sensor has no configurable attributes.
Reconfiguration
viam-server calls the reconfigure method when the user adds the model or changes its configuration.
The reconfiguration step serves two purposes:
Example module: For the camera model, the reconfigure method serves to set the image path for use in API methods.
Open 
Edit the reconfigure function to:
    def reconfigure(
        self, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]
    ):
        attrs = struct_to_dict(config.attributes)
        self.image_path = str(attrs.get("image_path"))
        return super().reconfigure(config, dependencies)
Add the following import to the top of the file:
from viam.utils import struct_to_dict
Open 
Add imagePath = "" to the global variables so you have the following:
var (
    HelloCamera      = resource.NewModel("exampleorg", "hello-world", "hello-camera")
    errUnimplemented = errors.New("unimplemented")
    imagePath        = ""
)
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.
Depending on the component API you are implementing, you can implement different API methods.
For each API method you want to implement, replace the body of the method with your relevant logic. Make sure you return the correct type in accordance with the function’s return signature. You can find details about the return types at python.viam.dev.
Example module: Implement the camera API and the sensor API:
The module generator created a stub for the get_images() function we want to implement in 
You need to replace raise NotImplementedError() with code to implement the method:
    async def get_images(
        self,
        *,
        filter_source_names: Optional[Sequence[str]] = None,
        extra: Optional[Dict[str, Any]] = None,
        timeout: Optional[float] = None,
        **kwargs
    ) -> Tuple[Sequence[NamedImage], ResponseMetadata]:
        img = Image.open(self.image_path)
        vi_img = pil_to_viam_image(img, CameraMimeType.JPEG)
        named = NamedImage("default", vi_img.data, vi_img.mime_type)
        metadata = ResponseMetadata()
        return [named], metadata
Add the following import to the top of the file:
from viam.media.utils.pil import pil_to_viam_image
from viam.media.video import CameraMimeType
from PIL import Image
Save the file.
Leave the rest of the camera API methods unimplemented. They do not apply to this camera.
Open 
Pillow
Next, implement the sensor API.
The module generator created a stub for the get_readings() function we want to implement in 
Replace raise NotImplementedError() with code to implement the method:
    async def get_readings(
        self,
        *,
        extra: Optional[Mapping[str, Any]] = None,
        timeout: Optional[float] = None,
        **kwargs
    ) -> Mapping[str, SensorReading]:
        number = random.random()
        return {
            "random_number": number
        }
Add the following import to the top of the file:
import random
Save the file.
Leave the rest of the sensor API methods unimplemented.
For each API method you want to implement, replace the body of the method with your relevant logic. Make sure you return the correct type in accordance with the function’s return signature. You can find details about the return types at go.viam.com/rdk/components.
Example module: Implement the camera API and the sensor API:
The module generator created a stub for the Images function we want to implement in 
You need to replace panic("not implemented") with code to implement the method:
func (s *helloWorldHelloCamera) Images(ctx context.Context, filterSourceNames []string, extra map[string]interface{}) ([]camera.NamedImage, resource.ResponseMetadata, error) {
    var responseMetadataRetVal resource.ResponseMetadata
    imgFile, err := os.Open(imagePath)
    if err != nil {
        return nil, responseMetadataRetVal, errors.New("Error opening image.")
    }
    defer imgFile.Close()
    imgByte, err := os.ReadFile(imagePath)
    if err != nil {
        return nil, responseMetadataRetVal, err
    }
    named, err := camera.NamedImageFromBytes(imgByte, "default", "image/png")
    if err != nil {
        return nil, responseMetadataRetVal, err
    }
    return []camera.NamedImage{named}, responseMetadataRetVal, nil
}
Add the following import at the top of 
"os"
Save the file.
Leave the rest of the camera API methods unimplemented. They do not apply to this camera.
Next, implement the sensor API.
The module generator created a stub for the Readings() function we want to implement in 
Replace panic("not implemented") with code to implement the method:
func (s *helloWorldHelloSensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) {
    number := rand.Float64()
    return map[string]interface{}{
        "random_number": number,
    }, nil
}
Add the following import to the list of imports at the top of 
"math/rand"
Since errUnimplemented and Config are defined in 
In 
Delete the "errors" import.
Search for and delete the line errUnimplemented = errors.New("unimplemented").
Search for type Config struct { and change it to type sensorConfig struct {.
Search for all instances of *Config in *sensorConfig.
Leave the rest of the sensor API methods unimplemented.
Save the file.
You can test your module locally before uploading it to the registry. You can configure it in the web UI using the local files on your machine.
To get your module onto your machine, hot reloading builds and packages it and then uses the shell service to copy it to the machine for testing.
If you are using a Python virtual environment (venv), make sure your module files are on the same device where viam-server is running, and add the module manually instead.
Run the following command to build the module and add it to your machine:
viam module reload-local --cloud-config /path/to/viam.json
viam module reload --part-id 123abc45-1234-432c-aabc-z1y111x23a00
For more information, see the viam module documentation.
You may need to refresh your machine page for your module to show up.
Navigate to your machine’s CONFIGURE page.
Click the + button, select Local module, then again select Local module.
Enter the path to the automatically-generated viam-server uses this path to start the module.
Example module:
For the hello-world module, the path should resemble /home/yourname/hello-world/run.sh on Linux, or /Users/yourname/hello-world/run.sh on macOS.
Save the config.
From within the module directory, compile your module with the module build command into a single executable:
viam module build local
Click the + button, select Local module, then again select Local module.
Enter the path to the viam-server uses this path to start the module.
Example module:
For the hello-world module, the path should resemble /home/yourname/hello-world/bin/hello-world.
Click Create.
Save the config.
Configure the model provided by your module
On your machine’s CONFIGURE page, click +, click Local module, then click Local component or Local service.
Select or enter the model namespace triplet, for example exampleorg:hello-world:hello-camera.
You can find the triplet in the model field of your 
Select the Type corresponding to the API you implemented.
Enter a Name such as camera-1.
Click Create.
Configure attributes
When you add a new component or service, a panel appears for it on the CONFIGURE tab.
If your model has required or optional attributes, configure them in the configuration field by adding them inside the {} object.
Example module: For the camera model, add the image_path attribute by replacing {} with:
{
  "image_path": "<replace with the path to your image>"
}
Save the config and wait a few seconds for it to apply.
Then click the TEST section of the camera’s configuration card. If there are errors you will see them on the configuration panel and on the LOGS tab.
Test the component
Click the TEST bar at the bottom of your modular component configuration, and check whether it works as expected.
Example module: For the camera model, the test panel should show the image:

If you also implemented the sensor model, add and test it the same way.
Iterate
If your component works, you’re almost ready to share your module by uploading it to the registry. If not, you have some debugging to do.
Each time you make changes to your local module code, you need to update its instance on your machine:
Run the reload command again to rebuild and restart your module:
viam module reload-local --cloud-config /path/to/viam.json
viam module reload --part-id 123abc45-1234-432c-aabc-z1y111x23a00
As you iterate, save the code changes, then restart the module in your machine’s CONFIGURE tab: In the upper-right corner of the module’s card, click … menu, then click Restart.

Run the following command to rebuild your module:
viam module build local
Then restart it in your machine’s CONFIGURE tab. In the upper-right corner of the module’s card, click … menu, then click Restart.

Once you have thoroughly tested your module, continue to package and deploy it.
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!