Part 3: Control Logic | Viam Documentation

Part 3: Control Logic

Goal: Write inspection logic that detects defective cans.

Skills: Generate module scaffolding using the Viam CLI, experience with Viam SDKs, develop code iteratively against remote hardware

Time: ~10 min

What You’ll Build

Your vision pipeline detects defective cans and records the results with images synced to the cloud. Now you’ll write a module that calls the vision service and exposes detection results through DoCommand. This detection data can drive dashboards, alerts, or—in a production system—trigger actuators to reject defective cans.

You’ll use the module-first development pattern: write code on your laptop, test it against remote hardware over the network. This workflow lets you iterate quickly—edit code, run it, see results—without redeploying after every change.

Prerequisites

This part of the tutorial requires the Go programming language and the Viam CLI.

Install Go

Check your Go version:

go version

You need Go 1.25.1 or later. If Go isn’t installed or is outdated, download it from go.dev/dl.

Install the Viam CLI

The Viam CLI is used for authentication, module generation, and deployment.

brew tap viamrobotics/brews
brew install viam
sudo curl -o /usr/local/bin/viam https://storage.googleapis.com/packages.viam.com/apps/viam-cli/viam-cli-stable-linux-amd64
sudo chmod +x /usr/local/bin/viam
Invoke-WebRequest -Uri "https://storage.googleapis.com/packages.viam.com/apps/viam-cli/viam-cli-stable-windows-amd64.exe" -OutFile "viam.exe"

Then run as . iam.exe or add the directory to your PATH.

Verify and Log In

Verify the CLI is installed:

viam version

Log in to Viam:

viam login

This stores credentials that your code will use to connect to remote machines.

Note

The Viam CLI (viam) is different from viam-server. The CLI runs on your development machine; viam-server runs on your robot/machine.

3.1 Generate the Module Scaffolding

A module in Viam is a package of code that adds capabilities to a machine. Modules run alongside viam-server and can provide custom components (like a new type of sensor) or services (like our inspection logic). By packaging code as a module, you can deploy it to any machine, share it with others, and manage versions through the Viam registry.

The Viam CLI can generate module boilerplate—saving you from writing registration code, build configuration, and project structure from scratch. This lets you focus on your business logic instead of infrastructure.

Set your organization’s namespace:

Before creating a module, your organization needs a public namespace. This is a unique identifier used in module names (for example, my-namespace:inspection-module).

  1. Click the organization dropdown in the upper right corner of the Viam app next to your initials
  2. Select Settings
  3. Click Set a public namespace and enter a unique name (lowercase letters, numbers, hyphens)
  4. Click Save

If your organization already has a namespace, you can skip this step.

Generate the module scaffolding:

viam module generate

Enter these values when prompted:

Prompt Value
Module name inspection-module
Language Go
Visibility Private
Namespace/Organization Select your organization
Resource type Generic Service
Model name inspector
Register module Yes

Why Generic Service? Viam has built-in APIs for hardware (camera, motor, arm). When your logic doesn’t fit those categories, Generic Service provides a flexible DoCommand interface—ideal for application-specific logic like inspection.

What does “Register module” do? Creates an entry in the Viam registry (just metadata, not your code). This enables cloud deployment later.

Files You’ll Work With

The generator creates a complete module structure. You’ll focus on three files:

This is the module-first development pattern: write logic in module.go, test locally with the CLI against your real machine, then deploy.

3.2 Add Remote Machine Connection

The generated CLI creates your service with empty dependencies—fine for testing logic in isolation, but useless for testing against real hardware. We’ll modify it to connect to your remote machine and access its resources. With this approach to Viam application development, your code runs locally on your laptop, but it talks to real cameras and other hardware your machine configuration includes.

Why is this valuable? Traditional embedded development requires: edit code → build → deploy → test → repeat. With module-first development: edit code → run locally → see results on real hardware. The iteration cycle drops from minutes to seconds.

Get your machine address:

  1. In the Viam app, go to your machine’s Configure page
  2. Click the Live dropdown
  3. Click Remote address to copy your machine address

Step 1: Connect to Your Machine

The generated realMain function creates your inspector with empty dependencies—useful for testing in isolation, but it can’t access your remote machine. We’ll replace it with code that:

Open cmd/cli/main.go and replace the import block with:

import (
    "context"
    "flag"
    "fmt"

"github.com/erh/vmodutils"
    "go.viam.com/rdk/logging"
)

Then replace the realMain function with:

func realMain() error {
    ctx := context.Background()
    logger := logging.NewLogger("cli")

host := flag.String("host", "", "Machine address (required)")
    flag.Parse()

if *host == "" {
        return fmt.Errorf("need -host flag (get address from Viam app)")
    }

logger.Infof("Connecting to %s...", *host)
    machine, err := vmodutils.ConnectToHostFromCLIToken(ctx, *host, logger)
    if err != nil {
        return fmt.Errorf("failed to connect: %w", err)
    }
    defer machine.Close(ctx)

logger.Info("Connected successfully!")
    return nil
}

Test the connection:

go run cmd/cli/main.go -host YOUR_MACHINE_ADDRESS

Step 2: Access Remote Resources

Now let’s verify we can access the camera on the remote machine.

Add the camera import:

At the top of cmd/cli/main.go, add this import:

"go.viam.com/rdk/components/camera"

Fetch the camera dependencies:

go mod tidy

Add camera access after the connection:

After the logger.Info("Connected successfully!") line, add:

// Get the camera from the remote machine
cam, err := camera.FromProvider(machine, "inspection-cam")
if err != nil {
    return fmt.Errorf("failed to get camera: %w", err)
}

// Capture an image
images, _, err := cam.Images(ctx, nil, nil)
if err != nil {
    return fmt.Errorf("failed to get images: %w", err)
}

if len(images) == 0 {
    return fmt.Errorf("no images returned from camera")
}

logger.Infof("Got image from camera: %s", images[0].SourceName)

Test resource access:

go mod tidy
go run cmd/cli/main.go -host YOUR_MACHINE_ADDRESS

Step 3: Add Detection Logic

Now we’ll implement the actual inspection logic. The generator created module.go with stub methods—we’ll fill them in to call the vision service and process results.

Step 1: Declare Dependencies

The Config struct tells Viam what resources your inspector needs. The Validate method returns those resource names so Viam knows to inject them.

In module.go, find the Config struct and Validate method and replace them with the code below.

type Config struct {
    Camera        string `json:"camera"`
    VisionService string `json:"vision"`
}

func (cfg *Config) Validate(path string) ([]string, []string, error) {
    if cfg.Camera == "" {
        return nil, nil, fmt.Errorf("camera is required")
    }
    if cfg.VisionService == "" {
        return nil, nil, fmt.Errorf("vision is required")
    }
    return []string{cfg.Camera, cfg.VisionService}, nil, nil
}

Step 2: Store Dependencies

Find the inspectionModuleInspector struct in module.go and add a detector field:

type inspectionModuleInspector struct {
    resource.AlwaysRebuild

name   resource.Name
    logger logging.Logger
    cfg    *Config

cancelCtx  context.Context
    cancelFunc func()

detector vision.Service  // Add this field
}

Step 3: Wire Dependencies

Update NewInspector in module.go:

func NewInspector(ctx context.Context, deps resource.Dependencies, name resource.Name, cfg *Config, logger logging.Logger) (resource.Resource, error) {
    cancelCtx, cancelFunc := context.WithCancel(context.Background())

detector, err := vision.FromProvider(deps, cfg.VisionService)
    if err != nil {
        return nil, fmt.Errorf("failed to get vision service %q: %w", cfg.VisionService, err)
    }

s := &inspectionModuleInspector{
        name:       name,
        logger:     logger,
        cfg:        cfg,
        cancelCtx:  cancelCtx,
        cancelFunc: cancelFunc,
        detector:   detector,
    }
    return s, nil
}

Step 4: Add Detection Logic

Add the detect method to module.go (lowercase—it’s internal):

func (s *inspectionModuleInspector) detect(ctx context.Context) (string, float64, error) {
    detections, err := s.detector.DetectionsFromCamera(ctx, s.cfg.Camera, nil)
    if err != nil {
        return "", 0, err
    }

if len(detections) == 0 {
        return "NO_DETECTION", 0, nil
    }

best := detections[0]
    for _, det := range detections[1:] {
        if det.Score() > best.Score() {
            best = det
        }
    }

return best.Label(), best.Score(), nil
}

Step 5: Update the CLI

Replace the realMain function:

func realMain() error {
    ctx := context.Background()
    logger := logging.NewLogger("cli")

host := flag.String("host", "", "Machine address (required)")
    flag.Parse()

if *host == "" {
        return fmt.Errorf("need -host flag (get address from Viam app)")
    }

cfg := &inspectionmodule.Config{
        Camera:        "inspection-cam",
        VisionService: "vision-service",
    }

deps, err := vmodutils.MachineToDependencies(machine)
    if err != nil {
        return fmt.Errorf("failed to get dependencies: %w", err)
    }

inspector, err := inspectionmodule.NewInspector(
        ctx,
        deps,
        generic.Named("inspector"),
        cfg,
        logger,
    )
    if err != nil {
        return fmt.Errorf("failed to create inspector: %w", err)
    }

result, err := inspector.DoCommand(ctx, map[string]interface{}{"detect": true})
    if err != nil {
        return fmt.Errorf("detection failed: %w", err)
    }

label := result["label"].(string)
    confidence := result["confidence"].(float64)
    logger.Infof("Detection: %s (%.1f%% confidence)", label, confidence*100)
    return nil
}

Test Detection

Fetch dependencies and run:

go mod tidy
go run ./cmd/cli/main.go -host YOUR_MACHINE_ADDRESS

You should see:

Connecting to your-machine-main.abc123.viam.cloud...
Detection: PASS (94.2% confidence)

3.4 Summary

You built a complete inspection system using the module-first development pattern:

  1. Generated the module scaffold—infrastructure handled, you focus on logic
  2. Connected to remote hardware from local code using vmodutils
  3. Implemented detection by calling the vision service and exposing results through DoCommand

The Key Insight

Your inspector code doesn’t know whether it’s running from the CLI on your laptop or deployed as a module on the machine. It just uses the dependencies it’s given. This abstraction is what makes rapid iteration possible during development and seamless deployment to production.

Your code is ready. In Part 4, you’ll deploy it to run on the machine and configure data capture for the detection results.