Previous
Integrate other hardware
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.
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.
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.
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, ".")
}
Replace the path in the script above with the path to where you saved your photo. Save the script.
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.
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.
For a quicker hello world experience, you can skip the sensor and only create a camera modular resource. If you prefer the simpler path, skip the sensor sections in the steps below.
The easiest way to generate the files for your module is to use the Viam CLI.
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.
Run the module generate
command in your terminal:
viam module generate
Follow the prompts, selecting the following options:
hello-world
Private
jessamy
.Camera Component
.
We will add the sensor later.hello-camera
No
Yes
Hit your Enter key and the generator will generate a folder called
Edit the stub files to add the logic from your test script in a way that works with the camera and sensor APIs:
First, implement the camera API methods by editing the camera class definition:
Add the following to the list of imports at the top of
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
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"))
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.")
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.
Open
Pillow
First, implement the camera API methods by editing the camera class definition:
Add the following to the list of imports at the top of
"os"
"reflect"
"io/ioutil"
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 = ""
)
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.
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
}
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
Delete the SubscribeRTP
and Unsubscribe
methods, since they are not applicable to this camera.
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")
}
Save the file.
With the implementation written, it’s time to test your module locally:
Create a virtual Python environment with the necessary packages by running the setup file from within the
sh setup.sh
This environment is where the local module will run.
viam-server
does not need to run inside this environment.
From within the
make setup
make build
Make sure your machine’s instance of viam-server
is live and connected to the Viam app.
In the Viam app, navigate to your machine’s CONFIGURE page.
Click the + button, select Local module, then again select Local module.
/Users/jessamyt/myCode/hello-world/run.sh
or /Users/jessamyt/myCode/hello-world/bin/hello-world
.
Click Create./Users/jessamyt/myCode/hello-world/bin/hello-world
.
Click Create.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
.
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"
.
Save the config, then click the TEST section of the camera’s configuration card.
You should see your image displayed. If not, check the LOGS tab for errors.
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.
To package (for Python) and upload your module and make it available to configure on machines in your organization:
Package the module as an archive, run the following command from inside the
tar -czf module.tar.gz run.sh setup.sh requirements.txt src
This creates a tarball called
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 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.
For more information about uploading modules, see Update and manage modules you created.
For more module creation information, see the Integrate other hardware guide.
To update or delete a module, see Update and manage modules.
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!