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.
In this page
Prerequisites
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.
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.
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.
Note
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.
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.
Run the
module generate
command in your terminal:viam module generate
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
.
- 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
- 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
- Module name:
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
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:
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
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 theimage_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 thevalidate()
function to throw an error ifimage_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
requirements.txt . Add the following line:Pillow
Implement the sensor API
Implement the camera API
First, implement the camera API methods by editing the camera class definition:
Add the following to the list of imports at the top of
hello-world/models/hello-camera.go :"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 theValidate
function with the following code to throw an error ifimage_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
andUnsubscribe
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 implementedStream
instead ofRead
.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.
Implement the sensor API
Test your module
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
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.
From within the
hello-world directory, compile your module into a single executable: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.
Enter the path to the automatically-generated
run.sh file, for example,/Users/jessamyt/myCode/hello-world/run.sh
. 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.
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.
To package (for Python) and upload your module and make it available to configure on machines in your organization:
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 .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 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.
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!