Previous
Part 2: Choose API
Part 3 of 5 | ⏱️ 30-40 minutes
GetImages methodAt the end of this part, you’ll have a working single-model module that returns images.
You’ll implement your module in three main steps:
When users add your module to their machine, they provide configuration in JSON format. Validation ensures:
For the example camera model, users will configure it like this:
{
"image_path": "/path/to/file.png"
}
Your validation must verify that image_path exists and is a string.
Model configuration happens in two steps:
The validation step serves two purposes:
viam-server will pass these resources to the next step as dependencies.
For more information, see Module dependencies.Open src/models/hello_camera.py and find the validate_config method (around line 38).
Replace it with:
@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 [], []
Code explanation:
image_path exists in the config(required_dependencies, optional_dependencies) - we have none for this simple cameraOpen hello-world/module.go and find the Validate function (around line 51).
Replace it with:
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 import at the top of the file:
"reflect"
Code explanation:
image_path is empty (required field)(required_deps, optional_deps, error) - we have no dependencies✅ Checkpoint 1: Configuration validation implemented
After validation succeeds, viam-server calls the reconfigure method. This is where you:
viam-server calls the reconfigure method when the user adds the model or changes its configuration.
The reconfiguration step serves two purposes:
Open src/models/hello_camera.py and find the reconfigure method (around line 51).
Replace it with:
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 import at the top of the file:
from viam.utils import struct_to_dict
Code explanation:
image_path as an instance variable for use in API methodsFor Go modules, configuration handling is done differently:
Open hello-world/module.go
Add imagePath = "" to the global variables (around line 18):
var (
HelloCamera = resource.NewModel("exampleorg", "hello-world", "hello-camera")
errUnimplemented = errors.New("unimplemented")
imagePath = ""
)
Edit the type Config struct definition (around line 32), replacing the comments with:
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 changes.
✅ Checkpoint 2: Reconfiguration implemented
Now comes the core functionality: implementing the methods from your chosen API.
The camera API has several methods, but you only need to implement the ones your hardware supports. For this example, we’ll implement GetImages (required) and leave others unimplemented.
Open src/models/hello_camera.py and find the get_images method (around line 74).
Replace raise NotImplementedError() with:
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 these imports at 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
Code explanation:
Add the Pillow dependency:
Open requirements.txt and add:
Pillow
Save the file.
Open hello-world/module.go and find the Images method (around line 111).
Replace panic("not implemented") with:
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 this import at the top of the file:
"os"
Code explanation:
The camera API includes methods like GetPointCloud, GetProperties, and DoCommand. You don’t need to implement all of them:
For this camera, we only implement GetImages because that’s all our hardware supports.
✅ Checkpoint 3: Camera implementation complete
✅ Module implemented:
✅ Understanding:
✅ Ready to test:
Your module is now ready to test! Continue to Part 4: Test your module locally to see your module in action.
Tutorial navigation:
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!