Previous
Integrate other hardware
This guide walks you through creating a modular camera component that returns 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, you will know how to create your own modular resources and package them into modules so you can use them on your machines.
This guide provides a basic learning example. For a more comprehensive guide including usage of cloud build tools for deployment across different platforms, see Integrate other hardware.
The functionality you want to add to your machine determines the APIs you need to implement, so let’s start by deciding what your module will do. For the purposes of this guide, you’re going to make a module that does two things:
Let’s figure out 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.
Each model can implement only one API, but your module can contain multiple modular resources. 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 steps below suggest that you disable cloud build when generating your stub files, for simplicity of local testing. If you plan to publish your module to the Viam registry, we recommend enabling cloud build, and then following the testing, packaging and uploading steps in Integrate other hardware once you are done writing your API implementation in this guide. Enabling cloud build will set up your module for a more automated deployment process if you plan to use your module for more than just learning.
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)
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"
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 := os.ReadFile(imagePath)
return imgByte, camera.ImageMetadata{}, nil
Delete the SubscribeRTP
and Unsubscribe
methods, since they are not applicable to this camera.
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
).
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) 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.
You can view the complete example code in the hello-world-module repository on GitHub.
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
viam module build local
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.
Enter the path to the automatically-generated /home/jessamy/hello-world/run.sh
on Linux or /Users/jessamy/hello-world/run.sh
.
Click Create.
Enter the path to the automatically-generated executable in the /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 can find your private module listed. With the module 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; local modules are primarily used for testing.
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!