diff --git a/assets/tutorials/first-project/awaiting-setup.png b/assets/tutorials/first-project/awaiting-setup.png new file mode 100644 index 0000000000..73e40edd17 Binary files /dev/null and b/assets/tutorials/first-project/awaiting-setup.png differ diff --git a/assets/tutorials/first-project/fleet-add-machine.png b/assets/tutorials/first-project/fleet-add-machine.png new file mode 100644 index 0000000000..1a367c295f Binary files /dev/null and b/assets/tutorials/first-project/fleet-add-machine.png differ diff --git a/assets/tutorials/first-project/sim-config-page.png b/assets/tutorials/first-project/sim-config-page.png new file mode 100644 index 0000000000..d033c96884 Binary files /dev/null and b/assets/tutorials/first-project/sim-config-page.png differ diff --git a/assets/tutorials/first-project/sim-config-running.png b/assets/tutorials/first-project/sim-config-running.png new file mode 100644 index 0000000000..a9c829574e Binary files /dev/null and b/assets/tutorials/first-project/sim-config-running.png differ diff --git a/assets/tutorials/first-project/sim-viewer-config-button.png b/assets/tutorials/first-project/sim-viewer-config-button.png new file mode 100644 index 0000000000..99588ba243 Binary files /dev/null and b/assets/tutorials/first-project/sim-viewer-config-button.png differ diff --git a/assets/tutorials/first-project/sim-viewer.png b/assets/tutorials/first-project/sim-viewer.png new file mode 100644 index 0000000000..1410640442 Binary files /dev/null and b/assets/tutorials/first-project/sim-viewer.png differ diff --git a/assets/tutorials/first-project/viam-app-live.png b/assets/tutorials/first-project/viam-app-live.png new file mode 100644 index 0000000000..3b0632e33e Binary files /dev/null and b/assets/tutorials/first-project/viam-app-live.png differ diff --git a/docs/operate/hello-world/first-project/gazebo-setup.md b/docs/operate/hello-world/first-project/gazebo-setup.md index f6ca59b172..0857ce0e44 100644 --- a/docs/operate/hello-world/first-project/gazebo-setup.md +++ b/docs/operate/hello-world/first-project/gazebo-setup.md @@ -13,166 +13,66 @@ This guide walks you through setting up the Gazebo simulation used in the [Your ## Prerequisites - **Docker Desktop** installed and running -- A free [Viam account](https://app.viam.com) - ~5GB disk space for the Docker image -## Step 1: Build the Docker Image +## Step 1: Pull the Docker Image The simulation runs in a Docker container with Gazebo Harmonic and viam-server pre-installed. -**Clone the simulation repository:** - -```bash -git clone https://github.com/viamrobotics/can-inspection-simulation.git -cd can-inspection-simulation -``` - -**Build the Docker image:** - ```bash -docker build -t gz-harmonic-viam . +docker pull ghcr.io/viamrobotics/can-inspection-simulation:latest ``` -This takes 5-10 minutes depending on your internet connection. - -## Step 2: Create a Machine in Viam - -1. Go to [app.viam.com](https://app.viam.com) and log in -2. Click the **Locations** tab -3. Click **+ Add machine** -4. Name it `inspection-station-1` -5. Click **Add machine** - -## Step 3: Create a credentials file - -1. Click the **Awaiting setup** button -2. Click **Machine cloud credentials** to copy your machine's credentials -3. In the `can-inspection-simulation` directory, create a file called `station1-viam.json` -4. Paste your machine's credentials into this file and save - -## Step 4: Start the Container +This downloads the pre-built image, which takes about a minute depending on your internet connection. -{{< tabs >}} -{{% tab name="Mac/Linux" %}} +## Step 2: Start the Container ```bash docker run --name gz-station1 -d \ -p 8080:8080 -p 8081:8081 -p 8443:8443 \ - -v "$(pwd)/station1-viam.json:/etc/viam.json" \ - gz-harmonic-viam + ghcr.io/viamrobotics/can-inspection-simulation:latest ``` -{{% /tab %}} -{{% tab name="Windows (PowerShell)" %}} - -```powershell -docker run --name gz-station1 -d ` - -p 8080:8080 -p 8081:8081 -p 8443:8443 ` - -v "${PWD}\station1-viam.json:/etc/viam.json" ` - gz-harmonic-viam -``` - -{{% /tab %}} -{{< /tabs >}} - -## Step 5: Verify the Setup - -**Check container logs:** - -```bash -docker logs gz-station1 -``` - -Look for: - -- "Can Inspection Station 1 Running!" -- viam-server startup messages - -**View the simulation:** +## Step 3: Verify the Simulation -Open your browser to `http://localhost:8081` +Open your browser to [http://localhost:8081](http://localhost:8081) -You should see a web-based 3D view of the inspection station with: +You should see two live camera feeds from the inspection station: -- A conveyor belt -- Cans moving along the belt -- An overhead camera view +{{}} -{{}} +## Step 4: Create a Machine in Viam -**Verify machine connection:** - -1. Go to [app.viam.com](https://app.viam.com) -2. Click on `inspection-station-1` -3. The status indicator should show **Live** (in green) - -## Troubleshooting - -{{< expand "Container won't start" >}} -**Check if ports are in use:** - -{{< tabs >}} -{{% tab name="Mac/Linux" %}} - -```bash -lsof -i :8080 -lsof -i :8081 -``` - -{{% /tab %}} -{{% tab name="Windows (PowerShell)" %}} - -```powershell -netstat -ano | findstr :8080 -netstat -ano | findstr :8081 -``` - -{{% /tab %}} -{{< /tabs >}} +1. Go to [app.viam.com](https://app.viam.com) and create a free account or log in +2. Click the **Locations** tab +3. Click **+ Add machine**, name it `inspection-station-1`, and click **Add machine** -If something is using these ports, stop it or use different port mappings. -{{< /expand >}} + {{}} -{{< expand "Machine shows Offline in Viam" >}} +## Step 5: Configure Machine Credentials -1. Check container is running: `docker ps` -2. Check logs for errors: `docker logs gz-station1` -3. Verify credentials in your config file match the Viam app -4. Try restarting: `docker restart gz-station1` - {{< /expand >}} +1. In the Viam app, click the **Awaiting setup** button on your new machine and click **Machine cloud credentials** to copy the credentials JSON -{{< expand "Simulation viewer is blank or slow" >}} + {{}} -- The web viewer requires WebGL support -- Try a different browser (Chrome usually works best) -- Check your system has adequate resources (4GB+ RAM recommended) - {{< /expand >}} +2. In the simulation viewer, click the **Configuration** button in the upper right corner -## Container Management + {{}} -**Stop the container:** +3. Paste your machine's credentials into the **Viam Configuration (viam.json)** text area and click **Update and Restart** -```bash -docker stop gz-station1 -``` + {{}} -**Start a stopped container:** - -```bash -docker start gz-station1 -``` + The status indicator will change to **Running** and a green banner will confirm the configuration was updated successfully. -**Remove the container (to recreate):** + {{}} -```bash -docker rm gz-station1 -``` +## Step 6: Verify Machine Connection -**View logs:** +Go back to your machine's page in the Viam app. +The status indicator should now show **Live**. -```bash -docker logs -f gz-station1 -``` +{{}} ## Ready to Continue diff --git a/docs/operate/hello-world/first-project/part-3.md b/docs/operate/hello-world/first-project/part-3.md index 2740bd6265..033554d249 100644 --- a/docs/operate/hello-world/first-project/part-3.md +++ b/docs/operate/hello-world/first-project/part-3.md @@ -4,36 +4,28 @@ title: "Part 3: Control Logic" weight: 30 layout: "docs" type: "docs" -description: "Write inspection logic that detects defective cans." +description: "Write inspection logic and run it against remote hardware from your laptop." date: "2025-01-30" --- -**Goal:** Write inspection logic that detects defective cans. +**Goal:** Write inspection logic and run it against remote hardware from your laptop. -**Skills:** Generate module scaffolding using the Viam CLI, experience with Viam SDKs, develop code iteratively against remote hardware +**Skills:** Write code against remote hardware, connect to a machine over the network, iterate rapidly without deploying, call a built-in service from code. -**Time:** ~15 min +**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. +Your vision pipeline detects defective cans and your data capture records the results. +Now you'll write the inspection logic 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. +You'll use the **module-first development pattern**: write code on your laptop, run it against real hardware over the network, and see results in seconds. +No deploying, no SSH, no waiting. +When the code is ready, it deploys to the machine without changes. ## Prerequisites -This part of the tutorial requires the Go programming language and the Viam CLI. - -### Install Go - -Check your Go version: - -```bash -go version -``` - -You need Go 1.21 or later. If Go isn't installed or is outdated, download it from [go.dev/dl](https://go.dev/dl/). - ### Install the Viam CLI The Viam CLI is used for authentication, module generation, and deployment. @@ -83,234 +75,182 @@ viam login This stores credentials that your code will use to connect to remote machines. {{< alert title="Note" color="info" >}} -The Viam CLI (`viam`) is different from `viam-server`. The CLI runs on your development machine; `viam-server` runs on your robot/machine. +The Viam CLI (`viam`) is different from `viam-server`. +The CLI runs on your development machine; `viam-server` runs on your robot/machine. {{< /alert >}} -## 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. Find **Public namespace** and enter a unique name (lowercase letters, numbers, hyphens) -4. Click **Save** +### Language Setup -{{}} +{{< tabs >}} +{{% tab name="Python" %}} -If your organization already has a namespace, you can skip this step. +**Install Python:** -**Generate the module scaffolding:** +Check your Python version: ```bash -viam module generate +python3 --version ``` -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: - -- **`module.go`**—Your service implementation. Contains Config, constructor, and methods. This is where your inspection logic goes. -- **`cmd/cli/main.go`**—CLI for local testing. We'll modify this to connect to your remote machine so you can test against real hardware without deploying. -- **`cmd/module/main.go`**—Entry point when deployed. Registers your service with viam-server. You won't modify this. +You need Python 3.8 or later. +If Python isn't installed or is outdated, download it from [python.org](https://www.python.org/downloads/). -This is the **module-first development pattern**: write logic in `module.go`, test locally with the CLI against your real machine, then deploy. +**Create an API key:** -## 3.2 Add Remote Machine Connection +The Python SDK authenticates with API keys (it does not support CLI token authentication). +Create one now: -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. +1. In the Viam app, click your machine's name to go to its page +2. Click the **API keys** tab +3. Click **Generate key** +4. Copy the **API key** and **API key ID** -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. +Set the environment variables in your terminal: -### 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: - -- Accepts a `-host` flag for your machine's address -- Uses your `viam login` credentials to authenticate -- Establishes a secure connection to the remote machine - -Open `cmd/cli/main.go` and replace the import block with: - -```go -import ( - "context" - "flag" - "fmt" - - "github.com/erh/vmodutils" - "go.viam.com/rdk/logging" -) +```bash +export VIAM_API_KEY="your-api-key" +export VIAM_API_KEY_ID="your-api-key-id" ``` -Then replace the `realMain` function with: - -```go -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)") - } +{{< alert title="Tip" color="tip" >}} +Add these exports to your shell profile (`.bashrc`, `.zshrc`) so they persist across terminal sessions. +{{< /alert >}} - 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) +**Clone the starter repo:** - logger.Info("Connected successfully!") - return nil -} +```bash +git clone https://github.com/viamrobotics/inspection-module-starter-python.git +cd inspection-module-starter-python +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt ``` -**Test the connection:** - -1. In the Viam app, go to your machine's **Configure** page -2. Click the **Online** dropdown -3. Click **Remote address** to copy your machine address - - {{}} - -4. ```bash - go run cmd/cli/main.go -host YOUR_MACHINE_ADDRESS - ``` - - You'll see WebRTC diagnostic output as the connection is established. Look for these messages confirming the connection: - - ```text - Connecting to your-machine.abc123.viam.cloud... - ... - successfully (re)connected to remote at address - ... - Connected successfully! - ``` +On Windows, activate the virtual environment with `venv\Scripts\activate` instead. - If you see an error, verify: +This repo contains a pre-built module with one method left for you to implement. - - Your machine is online (check the Viam app) - - You're logged in (`viam login`) - - The address is correct - -### Step 2: Access Remote Resources - -Now let's verify we can access the camera on the remote machine. +{{% /tab %}} +{{% tab name="Go" %}} -**Add the camera import:** +**Install Go:** -At the top of `cmd/cli/main.go`, add this import: +Check your Go version: -```go -"go.viam.com/rdk/components/camera" +```bash +go version ``` -**Fetch the camera dependencies:** +You need Go 1.21 or later. +If Go isn't installed or is outdated, download it from [go.dev/dl](https://go.dev/dl/). + +**Clone the starter repo:** ```bash +git clone https://github.com/viamrobotics/inspection-module-starter.git +cd inspection-module-starter go mod tidy ``` -**Add camera access after the connection:** - -After the `logger.Info("Connected successfully!")` line, add: +This repo contains a pre-built module with one method left for you to implement. -```go -// 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) -} +{{% /tab %}} +{{< /tabs >}} -// Capture an image -images, _, err := cam.Images(ctx, nil, nil) -if err != nil { - return fmt.Errorf("failed to get images: %w", err) -} +## 3.1 Why Start with a Module? -if len(images) == 0 { - return fmt.Errorf("no images returned from camera") -} +Viam development starts with modules, not scripts. +A **module** is a package of code that adds capabilities to a machine—custom components, services, or in this case, inspection logic. +By writing your code as a module from the start, the same code runs locally during development and on the machine in production. +There's no restructuring when you're ready to deploy. -logger.Infof("Got image from camera: %s", images[0].SourceName) -``` +This matters because the traditional embedded development loop—edit, build, deploy, test, repeat—is slow. +With a module, you edit code on your laptop, run it locally against remote hardware, and see results immediately. +The iteration cycle drops from minutes to seconds. -If your editor doesn't auto-format, run `gofmt -w cmd/cli/main.go` to fix indentation. +The starter repo includes everything you need: the module implementation, a deployment entry point, and a CLI for testing against remote hardware. +You'll implement one method, test it, iterate on it, and move on. +This is the development workflow you're used to, applied to physical devices. -**Test resource access:** +## 3.2 Explore the Starter Module -```bash -go mod tidy -go run cmd/cli/main.go -host YOUR_MACHINE_ADDRESS -``` +Before writing code, walk through what the starter repo provides. +You won't modify any of these files in this section—just read them to understand the structure. -You'll see the same WebRTC diagnostic output, followed by the camera confirmation: +### Service implementation -```text -Connected successfully! -Got image from camera: inspection-cam +{{< tabs >}} +{{% tab name="Python" %}} + +Open `src/models/inspector.py`. The key parts: + +**validate_config** declares the resources your inspector needs and returns them as dependencies: + +```python +@classmethod +def validate_config( + cls, config: ComponentConfig +) -> Tuple[Sequence[str], Sequence[str]]: + fields = config.attributes.fields + if "camera" not in fields or not fields["camera"].string_value: + raise Exception("camera is required") + if "vision" not in fields or not fields["vision"].string_value: + raise Exception("vision is required") + return [fields["camera"].string_value, fields["vision"].string_value], [] ``` -You've just accessed a camera on a remote machine from your local development environment. This same pattern works for any Viam resource—motors, sensors, vision services, or custom components. +The first element of the returned tuple lists required dependencies—Viam ensures these resources exist before creating your service. -{{< alert title="Takeaway" color="tip" >}} -The `vmodutils.ConnectToHostFromCLIToken` function uses your `viam login` credentials to establish a secure connection. Once connected, you access resources the same way you would in deployed code. This abstraction is what makes module-first development possible. -{{< /alert >}} +**reconfigure** wires the vision service by extracting it from the injected dependencies: -## 3.3 Add Detection Logic +```python +vision_resource_name = VisionClient.get_resource_name(vision_name) +self.detector = cast(VisionClient, dependencies[vision_resource_name]) +``` -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. +Your code declares what it needs in `validate_config`, and Viam provides it through dependency injection. +This means the same code works whether dependencies come from a remote machine (during development) or from viam-server (in production). -Complete all five steps below, then test at the end. +**do_command** dispatches to `detect`, the method you'll implement: -{{< alert title="Concept: Dependency Injection" color="info" >}} -Your inspector needs a vision service to detect cans. Rather than hardcoding how to find that service, you _declare_ the dependency in your Config, and Viam _injects_ it into your constructor. This means: +```python +async def do_command(self, command, **kwargs): + if "detect" in command: + label, confidence = await self.detect() + return {"label": label, "confidence": confidence} + raise Exception(f"unknown command: {command}") +``` -- Your code doesn't know where resources live (local or remote) -- The same code works in CLI testing and deployed modules - {{< /alert >}} +`do_command` is the public API for generic services. +External callers pass a command dict, and the method dispatches to internal logic. +This pattern keeps implementation details private while exposing a flexible interface. -### Step 1: Declare Dependencies +**The stub:** `detect()` currently raises an error: -The Config struct tells Viam what resources your inspector needs. The Validate method returns those resource names so Viam knows to inject them. +```python +async def detect(self) -> Tuple[str, float]: + raise NotImplementedError("not implemented: fill in the detect method") +``` -Remember in Part 1 when you configured the camera with `"id": "/inspection_camera"`? When you add the service we're building now to your machine (in Part 4 of this tutorial), you'll configure it with attributes for the camera and vision service to use. +This is what you'll implement in the next section. -The Config struct in the code below specifies what the names of those attributes will be in the configuration JSON you'll update after including this module in your machine config. +{{% /tab %}} +{{% tab name="Go" %}} -In `module.go`, find the `Config` struct and `Validate` method and replace them with the code below. +Open `module.go`. The key parts: + +**Config** declares the resources your inspector needs—a camera and a vision service: ```go type Config struct { Camera string `json:"camera"` VisionService string `json:"vision"` } +``` + +**Validate** returns these as dependencies so Viam injects them before creating your service: +```go func (cfg *Config) Validate(path string) ([]string, []string, error) { if cfg.Camera == "" { return nil, nil, fmt.Errorf("camera is required") @@ -324,71 +264,135 @@ func (cfg *Config) Validate(path string) ([]string, []string, error) { The first return value from Validate lists dependencies—Viam ensures these resources exist before creating your service. -### Step 2: Store Dependencies - -The generated struct needs a field to hold the vision service that Viam will inject. - -Find the `inspectionModuleInspector` struct in `module.go` and add a `detector` field: +**NewInspector** wires the vision service by extracting it from the injected dependencies: ```go -type inspectionModuleInspector struct { - resource.AlwaysRebuild +detector, err := vision.FromProvider(deps, cfg.VisionService) +``` - name resource.Name - logger logging.Logger - cfg *Config +Your code declares what it needs in Config, and Viam provides it through dependency injection. +This means the same code works whether dependencies come from a remote machine (during development) or from viam-server (in production). - cancelCtx context.Context - cancelFunc func() +**DoCommand** dispatches to `detect`, the method you'll implement: - detector vision.Service // Add this field +```go +func (s *inspectionModuleInspector) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { + if _, ok := cmd["detect"]; ok { + label, confidence, err := s.detect(ctx) + if err != nil { + return nil, err + } + return map[string]interface{}{ + "label": label, + "confidence": confidence, + }, nil + } + return nil, fmt.Errorf("unknown command: %v", cmd) } ``` -The `detector` field will hold a reference to the vision service. +`DoCommand` is the public API for generic services. +External callers pass a command map, and the method dispatches to internal logic. +This pattern keeps implementation details private while exposing a flexible interface. -Add the vision import to your import block: +**The stub:** `detect()` currently returns an error: ```go -"go.viam.com/rdk/services/vision" +func (s *inspectionModuleInspector) detect(ctx context.Context) (string, float64, error) { + return "", 0, fmt.Errorf("not implemented: fill in the detect method") +} ``` -### Step 3: Wire Dependencies +This is what you'll implement in the next section. -The constructor extracts the vision service from the dependencies map and stores it in the struct. This validates that the vision service exists on the machine and that methods on that struct in the module we're writing have access to it. +{{% /tab %}} +{{< /tabs >}} -Update `NewInspector` in `module.go`: +### CLI -```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()) +{{< tabs >}} +{{% tab name="Python" %}} - // --- Add this block --- - detector, err := vision.FromProvider(deps, cfg.VisionService) - if err != nil { - return nil, fmt.Errorf("failed to get vision service %q: %w", cfg.VisionService, err) - } - // --- End add --- - - s := &inspectionModuleInspector{ - name: name, - logger: logger, - cfg: cfg, - cancelCtx: cancelCtx, - cancelFunc: cancelFunc, - detector: detector, // Add this field - } - return s, nil -} -``` +The CLI (`cli.py`) connects to your remote machine and runs the inspector against it. +It: -`vision.FromProvider` looks up the resource by name and returns it as the correct type. If the resource doesn't exist or isn't a vision service, it returns an error. +1. Accepts a `--host` argument for your machine's address +2. Reads `VIAM_API_KEY` and `VIAM_API_KEY_ID` from environment variables +3. Connects using `RobotClient.Options.with_api_key()` +4. Builds dependencies from the remote machine's resources +5. Creates an inspector instance and calls `do_command({"detect": True})` -### Step 4: Add Detection Logic +{{< alert title="Resource names" color="info" >}} +The CLI hardcodes the camera and vision service names (`inspection-cam` and `can-detector`). +These must match the names you configured in Part 1. +If you used different names, update the `config` in `cli.py` before running. +{{< /alert >}} + +{{% /tab %}} +{{% tab name="Go" %}} -Now add the internal method that performs inspection, and update `DoCommand` to expose it. +The CLI (`cmd/cli/main.go`) connects to your remote machine and runs the inspector against it. +It: -Add the `detect` method to `module.go` (lowercase—it's internal): +1. Accepts a `-host` flag for your machine's address +2. Uses your `viam login` credentials to authenticate through `vmodutils.ConnectToHostFromCLIToken` +3. Converts the machine connection into dependencies your inspector can use +4. Creates an inspector instance and calls `DoCommand({"detect": true})` + +{{< alert title="Resource names" color="info" >}} +The CLI hardcodes the camera and vision service names (`inspection-cam` and `can-detector`). +These must match the names you configured in Part 1. +If you used different names, update the `cfg` struct in `cmd/cli/main.go` before running. +{{< /alert >}} + +{{% /tab %}} +{{< /tabs >}} + +### Module entry point + +{{< tabs >}} +{{% tab name="Python" %}} + +`src/main.py` is the module entry point for deployment. +When your module runs on the machine, this file registers your service with viam-server through `Module.run_from_registry()`. +You won't modify this file. + +{{% /tab %}} +{{% tab name="Go" %}} + +`cmd/module/main.go` is the module entry point for deployment. +When your module runs on the machine, this file registers your service with viam-server. +You won't modify this file. + +{{% /tab %}} +{{< /tabs >}} + +## 3.3 Implement Detection + +{{< tabs >}} +{{% tab name="Python" %}} + +Open `src/models/inspector.py` and find the `detect` method stub. Replace it with: + +```python +async def detect(self) -> Tuple[str, float]: + detections = await self.detector.get_detections_from_camera(self.camera_name) + + if len(detections) == 0: + return ("NO_DETECTION", 0.0) + + best = max(detections, key=lambda d: d.confidence) + return (best.class_name, best.confidence) +``` + +`get_detections_from_camera` tells the vision service which camera to use. +The vision service grabs an image, runs the ML model, and returns structured detection results. +Python's `max()` with a `key` function finds the highest-confidence detection. + +{{% /tab %}} +{{% tab name="Go" %}} + +Open `module.go` and find the `detect` method stub. Replace it with: ```go func (s *inspectionModuleInspector) detect(ctx context.Context) (string, float64, error) { @@ -412,171 +416,156 @@ func (s *inspectionModuleInspector) detect(ctx context.Context) (string, float64 } ``` -`DetectionsFromCamera` tells the vision service which camera to use. The vision service grabs an image, runs the ML model, and returns structured detection results. We find the highest-confidence detection and return its label and score. +`DetectionsFromCamera` tells the vision service which camera to use. +The vision service grabs an image, runs the ML model, and returns structured detection results. +The code finds the highest-confidence detection and returns its label and score. -Now update the generated `DoCommand` stub to dispatch to `detect`: +{{% /tab %}} +{{< /tabs >}} -```go -func (s *inspectionModuleInspector) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) { - if _, ok := cmd["detect"]; ok { - label, confidence, err := s.detect(ctx) - if err != nil { - return nil, err - } - return map[string]interface{}{ - "label": label, - "confidence": confidence, - }, nil - } - return nil, fmt.Errorf("unknown command: %v", cmd) -} -``` +### Run it -`DoCommand` is the public API for generic services. External callers pass a command map, and the method dispatches to internal logic. This pattern keeps implementation details private while exposing a flexible interface. +1. In the Viam app, go to your machine's **Configure** page +2. Click the **Online** dropdown +3. Click **Remote address** to copy your machine address -### Step 5: Update the CLI + {{}} -Now wire everything together in `cmd/cli/main.go` so we can test. This replaces the camera test code from section 3.2. +4. Run: -Replace the `realMain` function: +{{< tabs >}} +{{% tab name="Python" %}} -```go -func realMain() error { - ctx := context.Background() - logger := logging.NewLogger("cli") + ```bash + python cli.py --host YOUR_MACHINE_ADDRESS + ``` - host := flag.String("host", "", "Machine address (required)") - flag.Parse() +{{% /tab %}} +{{% tab name="Go" %}} - if *host == "" { - return fmt.Errorf("need -host flag (get address from Viam app)") - } + ```bash + go run ./cmd/cli -host YOUR_MACHINE_ADDRESS + ``` - 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) +{{% /tab %}} +{{< /tabs >}} - cfg := &inspectionmodule.Config{ - Camera: "inspection-cam", - VisionService: "vision-service", - } + You should see: - deps, err := vmodutils.MachineToDependencies(machine) - if err != nil { - return fmt.Errorf("failed to get dependencies: %w", err) - } + ```text + Connecting to your-machine-main.abc123.viam.cloud... + Detection: PASS (94.2% confidence) + ``` - inspector, err := inspectionmodule.NewInspector( - ctx, - deps, - generic.Named("inspector"), - cfg, - logger, - ) - if err != nil { - return fmt.Errorf("failed to create inspector: %w", err) - } + Run it several times—results change as different cans pass under the camera. - result, err := inspector.DoCommand(ctx, map[string]interface{}{"detect": true}) - if err != nil { - return fmt.Errorf("detection failed: %w", err) - } +{{< alert title="What just happened" color="info" >}} +Your laptop connected to the remote machine, your code called the vision service, and the vision service ran ML inference on an image from the camera. +The code runs locally but uses remote hardware—this is the module-first pattern in action. +{{< /alert >}} - label := result["label"].(string) - confidence := result["confidence"].(float64) - logger.Infof("Detection: %s (%.1f%% confidence)", label, confidence*100) - return nil -} -``` +{{< expand "Troubleshooting" >}} +**"failed to connect" or timeout errors:** -The CLI creates the inspector with dependencies from the remote machine, then calls `DoCommand` with `{"detect": true}`. This is the same pattern used in production Viam modules, where `DoCommand` is the public API for generic services. +- Verify your machine is online in the Viam app +- Confirm the host address is correct +- **Go:** Check that you've run `viam login` successfully +- **Python:** Verify `VIAM_API_KEY` and `VIAM_API_KEY_ID` are set (`echo $VIAM_API_KEY`) -Update the imports: +**"failed to get vision service" or "not found in dependencies" error:** -```go -import ( - "context" - "flag" - "fmt" - - "inspectionmodule" - "github.com/erh/vmodutils" - "go.viam.com/rdk/logging" - "go.viam.com/rdk/services/generic" -) -``` +- Verify the vision service name in your CLI file matches your machine config from Part 1 +- Check the exact name—it's case-sensitive + +**"NO_DETECTION" result:** -### Test Detection +- Normal if no can is in view—wait for one to appear +- Check the camera is working in the Viam app's Test panel -Fetch dependencies and run: +**Python: ModuleNotFoundError or ImportError:** -```bash -go mod tidy -go run ./cmd/cli -host YOUR_MACHINE_ADDRESS -``` +- Verify your virtual environment is activated (`source venv/bin/activate`) +- Verify dependencies are installed (`pip install -r requirements.txt`) +- Run from the repo root directory +{{< /expand >}} + +## 3.4 Iterate — Filter for Failures -You should see: +Your inspector reports every detection—PASS and FAIL. +In an inspection system, you often only care about failures. +Let's filter for them. -```text -Connecting to your-machine-main.abc123.viam.cloud... -Detection: PASS (94.2% confidence) +{{< tabs >}} +{{% tab name="Python" %}} + +In `src/models/inspector.py`, add a check after finding the highest-confidence detection. Insert these lines just before the final `return` in `detect`: + +```python + # Only report failures — treat PASS as no detection + if best.class_name != "FAIL": + return ("NO_DETECTION", 0.0) ``` -Run it several times—results change as different cans pass under the camera. +Run again: -{{< alert title="What just happened" color="info" >}} -Your laptop connected to the remote machine, your code called the vision service, and the vision service ran ML inference on an image from the camera. The code runs locally but uses remote hardware—this is the module-first pattern in action. -{{< /alert >}} +```bash +python cli.py --host YOUR_MACHINE_ADDRESS +``` -{{< expand "Troubleshooting" >}} -**"failed to connect" or timeout errors:** +{{% /tab %}} +{{% tab name="Go" %}} -- Verify your machine is online in the Viam app -- Check that you've run `viam login` successfully -- Confirm the host address is correct +In `module.go`, add a check after finding the highest-confidence detection. Insert these lines just before the final `return` in `detect`: -**"failed to get vision service" error:** +```go + // Only report failures — treat PASS as no detection + if best.Label() != "FAIL" { + return "NO_DETECTION", 0, nil + } +``` -- Verify `vision-service` exists in your machine config (Part 1) -- Check the exact name matches—it's case-sensitive +Run again: -**"NO_DETECTION" result:** +```bash +go run ./cmd/cli -host YOUR_MACHINE_ADDRESS +``` -- Normal if no can is in view—wait for one to appear -- Check the camera is working in the Viam app's Test panel - {{< /expand >}} +{{% /tab %}} +{{< /tabs >}} -{{< alert title="Checkpoint" color="success" >}} -You can now detect cans from your laptop. You declared dependencies in Config, returned them from Validate, and extracted them in the constructor. This pattern works for any Viam resource. -{{< /alert >}} +Now it only flags defective cans. +PASS detections are treated as no detection. -## 3.4 Summary +Edit, run, see results on real hardware—seconds, not minutes. +This is the rapid iteration loop that module-first development gives you. -You built a complete inspection system using the module-first development pattern: +Before continuing to Part 4, **remove the filter** so the inspector reports all detections. Delete the filter block you just added, leaving the method as it was after section 3.3. -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` +## 3.5 Summary -{{< alert title="Extending the inspector" color="tip" >}} -In a production system, you could extend `DoCommand` to trigger actuators—for example, a motor that pushes defective cans off the belt. The same dependency injection pattern applies: declare the motor in Config, extract it in the constructor, and call it from your detection logic. -{{< /alert >}} +You wrote inspection logic and ran it against remote hardware from your laptop: + +1. **Connected** to a remote machine over the network—no VPN, no SSH +2. **Implemented detection** by calling a built-in vision service from code +3. **Iterated rapidly** by editing and re-running—seconds, not minutes +4. **Used the module-first pattern**—the same code deploys to the machine without changes ### What You Learned | Concept | What It Means | Where You'll Use It | | ---------------------------- | ----------------------------------------------------- | ---------------------------------------- | -| **Module-first development** | Test against real hardware without deploying | Any time you're developing control logic | +| **Remote development** | Code on your laptop talks to real hardware | Any time you're developing control logic | +| **Module-first development** | Same code works in dev and production | Every module you build | | **Dependency injection** | Declare what you need, let Viam provide it | Every module you build | | **DoCommand pattern** | Expose functionality through a flexible map-based API | Any generic service | ### 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 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. +**Your code is ready.** +In Part 4, you'll deploy it to run on the machine and configure data capture for the detection results. **[Continue to Part 4: Deploy a Module →](../part-4/)** diff --git a/docs/operate/hello-world/first-project/part-4.md b/docs/operate/hello-world/first-project/part-4.md index 5d900fff3f..cd8791354b 100644 --- a/docs/operate/hello-world/first-project/part-4.md +++ b/docs/operate/hello-world/first-project/part-4.md @@ -16,19 +16,47 @@ date: "2025-01-30" ## What You'll Do -In Part 3, you built inspection logic that runs from your laptop. That's great for development, but in production the code needs to run on the machine itself—so it works even when your laptop is closed. +In Part 3, you built inspection logic that runs from your laptop. +That's great for development, but in production the code needs to run on the machine itself—so it works even when your laptop is closed. -The module generator already created most of what you need: +The starter repo already created most of what you need: -- `cmd/module/main.go`—module entry point +- A module entry point that connects your service to viam-server - `meta.json`—registry metadata -- Model registration in `init()` +- Model registration so viam-server can create instances of your service -You just need to build, package, and deploy. +You just need to set your namespace, build, package, and deploy. -## 4.1 Review the Generated Module Structure +## 4.1 Review the Module Structure -The generator already created everything needed to run as a module. Let's review what's there. +The starter repo already includes everything needed to run as a module. +Let's review what's there. + +### Module entry point + +{{< tabs >}} +{{% tab name="Python" %}} + +**`src/main.py`** + +```python +import asyncio +from viam.module.module import Module +try: + from models.inspector import Inspector +except ModuleNotFoundError: + from .models.inspector import Inspector + +if __name__ == '__main__': + asyncio.run(Module.run_from_registry()) +``` + +`Module.run_from_registry()` connects your module to viam-server. +The import of `Inspector` is what triggers model registration—when Python loads the class, `EasyResource` automatically registers it. +You don't need to modify this file. + +{{% /tab %}} +{{% tab name="Go" %}} **`cmd/module/main.go`** @@ -40,18 +68,43 @@ func main() { } ``` -This connects your module to viam-server and registers the inspector model. When you add this service to your machine configuration, viam-server uses this entry point to create and manage instances. You don't need to modify it. +This connects your module to viam-server and registers the inspector model. +When you add this service to your machine configuration, viam-server uses this entry point to create and manage instances. +You don't need to modify this file. + +{{% /tab %}} +{{< /tabs >}} {{< alert title="What is a model?" color="info" >}} -In Viam, a _model_ is a specific implementation of an API—identified by a triplet like `your-namespace:inspection-module:inspector`. This can be confusing because we also refer to ML models as "models." When you see "model" in the context of modules and resources, it means the implementation type, not a machine learning model. +In Viam, a _model_ is a specific implementation of an API—identified by a triplet like `your-namespace:inspection-module:inspector`. +This can be confusing because we also refer to ML models as "models." +When you see "model" in the context of modules and resources, it means the implementation type, not a machine learning model. {{< /alert >}} -**`module.go`** provides model registration in `init()`: +### Model registration + +{{< tabs >}} +{{% tab name="Python" %}} + +In Python, model registration is automatic. +When `Inspector` subclasses `EasyResource`, the `EasyResource.__init_subclass__` hook registers the model with viam-server using the `MODEL` class variable: + +```python +class Inspector(Generic, EasyResource): + MODEL: ClassVar[Model] = Model( + ModelFamily("stations", "inspection-module"), "inspector" + ) +``` + +No explicit registration function needed—the class definition itself is the registration. + +{{% /tab %}} +{{% tab name="Go" %}} -The generator created an `init()` function that registers your model: +**`module.go`** provides model registration in `init()`: ```go -var Inspector = resource.NewModel("your-namespace", "inspection-module", "inspector") +var Inspector = resource.NewModel("stations", "inspection-module", "inspector") func init() { resource.RegisterService(generic.API, Inspector, @@ -65,45 +118,143 @@ func init() { Note the two uses of "inspector" here: - `Inspector` (capital I)—the Go variable name, exported so `main.go` can reference it -- `"inspector"` (lowercase, in quotes)—a string that becomes the third part of the model triplet `your-namespace:inspection-module:inspector` +- `"inspector"` (lowercase, in quotes)—a string that becomes the third part of the model triplet `stations:inspection-module:inspector` This `init()` function runs automatically when the module starts, telling viam-server how to create instances of your service. -**`meta.json`** Registry metadata: +{{% /tab %}} +{{< /tabs >}} + +### Registry metadata + +{{< tabs >}} +{{% tab name="Python" %}} + +**`meta.json`** ```json { - "module_id": "your-namespace:inspection-module", + "module_id": "stations:inspection-module", "visibility": "private", "models": [ { "api": "rdk:service:generic", - "model": "your-namespace:inspection-module:inspector" + "model": "stations:inspection-module:inspector" } ], + "entrypoint": "./run.sh" +} +``` + +The `entrypoint` points to `run.sh`, which creates a virtual environment, installs dependencies, and starts the module. +The `models` array is pre-populated—it tells the registry what your module provides. + +{{% /tab %}} +{{% tab name="Go" %}} + +**`meta.json`** + +```json +{ + "module_id": "stations:inspection-module", + "visibility": "private", "entrypoint": "bin/inspection-module" } ``` -This tells the registry what your module provides. +The `entrypoint` is the compiled binary. +The `models` array will be populated by `update-models` during the build process. + +{{% /tab %}} +{{< /tabs >}} {{< alert title="The key pattern" color="tip" >}} -The generator created module infrastructure. You added business logic (`detect`) and exposed it through `DoCommand`. The same `NewInspector` constructor works for both CLI testing and module deployment. +The starter repo created module infrastructure. +You added business logic (`detect`) and exposed it through `DoCommand`. +The same constructor works for both CLI testing and module deployment. {{< /alert >}} ## 4.2 Build and Upload Your Module -**Update the module's model list:** +### Set your namespace -The module was already registered in the Viam registry when you ran `viam module generate` in Part 3 (you answered "Yes" to "Register module"). Now you need to update its model metadata. First, build a local binary: +The starter repos use `stations` as a placeholder namespace. +You need to replace it with your organization's public namespace so the module is registered under your account. + +1. Find your public namespace: + + ```bash + viam organizations list + ``` + + Look for the `public_namespace` value for your organization. + If you don't have one set, go to your organization's settings page in the Viam app to create one. + +2. Update `meta.json`—replace `stations` with your namespace in both the `module_id` and (if present) the `model` field: + + ```json + { + "module_id": "YOUR-NAMESPACE:inspection-module", + ... + "model": "YOUR-NAMESPACE:inspection-module:inspector" + } + ``` + +3. Update the model triplet in your source code to match: + +{{< tabs >}} +{{% tab name="Python" %}} + + In `src/models/inspector.py`, update the `MODEL` definition: + + ```python + MODEL: ClassVar[Model] = Model( + ModelFamily("YOUR-NAMESPACE", "inspection-module"), "inspector" + ) + ``` + +{{% /tab %}} +{{% tab name="Go" %}} + + In `module.go`, update the model variable: + + ```go + var Inspector = resource.NewModel("YOUR-NAMESPACE", "inspection-module", "inspector") + ``` + +{{% /tab %}} +{{< /tabs >}} + +### Register the module + +Create the module entry in the Viam registry: ```bash -make +viam module create --module-path meta.json ``` -Then run `update-models` to detect which models your module provides and update `meta.json`: +### Build and package + +{{< tabs >}} +{{% tab name="Python" %}} + +Build the module archive: ```bash +./build.sh +``` + +This creates a virtual environment, installs PyInstaller, bundles your code into a standalone binary, and packages it as `dist/archive.tar.gz`. + +{{% /tab %}} +{{% tab name="Go" %}} + +**Update the module's model list:** + +Build a local binary, then run `update-models` to detect which models your module provides and update `meta.json`: + +```bash +make viam module update-models --binary /full/path/to/bin/inspection-module ``` @@ -111,7 +262,9 @@ Replace `/full/path/to/` with the absolute path to your module directory. **Cross-compile for the target platform:** -Your module will run inside a Linux container, not on your development machine. Even if your Mac and the container both use ARM processors, a macOS binary won't run on Linux—you need to cross-compile. Cross-compilation also requires disabling CGO (Go's C interop) and using the `no_cgo` build tag. +Your module will run inside a Linux container, not on your development machine. +Even if your Mac and the container both use ARM processors, a macOS binary won't run on Linux—you need to cross-compile. +Cross-compilation also requires disabling CGO (Go's C interop) and using the `no_cgo` build tag. {{< tabs >}} {{% tab name="Mac (Apple Silicon)" %}} @@ -161,43 +314,47 @@ No cross-compilation needed—you're already on Linux. tar czf module.tar.gz meta.json bin/ ``` -**Upload to the registry:** +{{% /tab %}} +{{< /tabs >}} + +### Upload to the registry -The `--platform` flag must match the architecture you compiled for: +The `--platform` flag must match your machine's architecture. +Replace `` with your archive path from the previous step (`dist/archive.tar.gz` for Python, `module.tar.gz` for Go): {{< tabs >}} {{% tab name="Mac (Apple Silicon)" %}} ```bash -viam module upload --version 0.0.1 --platform linux/arm64 --upload module.tar.gz +viam module upload --version 0.0.1 --platform linux/arm64 --upload ``` {{% /tab %}} {{% tab name="Mac (Intel)" %}} ```bash -viam module upload --version 0.0.1 --platform linux/amd64 --upload module.tar.gz +viam module upload --version 0.0.1 --platform linux/amd64 --upload ``` {{% /tab %}} {{% tab name="Windows" %}} ```bash -viam module upload --version 0.0.1 --platform linux/amd64 --upload module.tar.gz +viam module upload --version 0.0.1 --platform linux/amd64 --upload ``` {{% /tab %}} {{% tab name="Linux (Intel/AMD)" %}} ```bash -viam module upload --version 0.0.1 --platform linux/amd64 --upload module.tar.gz +viam module upload --version 0.0.1 --platform linux/amd64 --upload ``` {{% /tab %}} {{% tab name="Linux (ARM)" %}} ```bash -viam module upload --version 0.0.1 --platform linux/arm64 --upload module.tar.gz +viam module upload --version 0.0.1 --platform linux/arm64 --upload ``` {{% /tab %}} @@ -244,15 +401,19 @@ Click **Save**. {{}} -The module is now ready. You'll configure automatic detection in the next section. +The module is now ready. +You'll configure automatic detection in the next section. ## 4.4 Configure Detection Data Capture -In Part 2, you captured images from the vision service. Those images are great for visual review, but they're binary data—you can't query them with SQL. Now you'll configure **tabular data capture** on your inspector's DoCommand, which will let you query detection results. +In Part 2, you captured images from the vision service. +Those images are great for visual review, but they're binary data—you can't query them with SQL. +Now you'll configure **tabular data capture** on your inspector's DoCommand, which will let you query detection results. **Add inspector-service as a data manager dependency:** -The data manager needs to know about your inspector service before it can capture data from it. You'll add this dependency in the JSON configuration. +The data manager needs to know about your inspector service before it can capture data from it. +You'll add this dependency in the JSON configuration. 1. In the **Configure** tab, click **JSON** in the upper left 2. Find the `data-service` entry in the `services` array @@ -323,7 +484,8 @@ After a few minutes of data collection, you can query the results: **Understanding the data structure:** -Each captured detection is stored as a JSON document. Here's what the data looks like: +Each captured detection is stored as a JSON document. +Here's what the data looks like: ```json { @@ -356,7 +518,9 @@ The key fields for analysis are nested under `data.docommand_output`: **Querying with MQL:** -You can also query using MQL (MongoDB Query Language), which is useful for aggregations. Select **MQL** in the Query interface. For example, to count failures by hour: +You can also query using MQL (MongoDB Query Language), which is useful for aggregations. +Select **MQL** in the Query interface. +For example, to count failures by hour: ```javascript [ @@ -391,8 +555,8 @@ The images show what the system saw; the tabular data tracks what it decided. You deployed your inspection logic as a Viam module: -1. **Reviewed**—the generator already created module structure and registration -2. **Deployed**—built, packaged, uploaded, configured +1. **Reviewed**—the starter repo already provided module structure and registration +2. **Deployed**—set your namespace, built, packaged, uploaded, configured 3. **Configured data capture**—detection results are now queryable **The development pattern:**