diff --git a/docs/operate/modules/support-hardware/_index.md b/docs/operate/modules/support-hardware/_index.md
index a2c0a7094b..1a3e28d009 100644
--- a/docs/operate/modules/support-hardware/_index.md
+++ b/docs/operate/modules/support-hardware/_index.md
@@ -1,6 +1,6 @@
---
-title: "Support additional hardware and software"
-linkTitle: "Support hardware"
+title: "Create a custom module to support additional hardware"
+linkTitle: "Create custom modules"
weight: 30
layout: "docs"
type: "docs"
@@ -40,1122 +40,126 @@ aliases:
If your physical or virtual hardware is not supported by an existing registry {{< glossary_tooltip term_id="module" text="module" >}}, you can create a new module to add support for it.
-{{% hiddencontent %}}
-If you want to create a "custom module", this page provides instructions for creating one in Python and Go.
-{{% /hiddencontent %}}
+This tutorial series walks you through creating a custom module in Python or Go.
-This page provides instructions for creating a module in Python or Go.
-For C++ module examples, see the [C++ examples directory on GitHub](https://github.com/viamrobotics/viam-cpp-sdk/tree/main/src/viam/examples/).
-If you want to create a module for use with a microcontroller, see [Modules for ESP32](/operate/modules/advanced/micro-module/).
+## What you'll build
-**Example module:** With each step of this guide, you have instructions for creating a {{< glossary_tooltip term_id="module" text="module" >}} which does two things:
+In this tutorial series, you'll create a **hello-world module** with two models:
-1. Gets an image from a configured path on your machine
-2. Returns a random number
+1. **Camera model** (`hello-camera`): Returns an image from a configured file path on your machine
+2. **Sensor model** (`hello-sensor`): Returns a random number
-## Prerequisites
+You'll learn how to:
+- Map hardware functionality to Viam APIs
+- Implement configuration validation
+- Create modules with multiple models
+- Test modules locally before deployment
-{{< expand "Install the Viam CLI and authenticate" >}}
-Install the Viam CLI and authenticate to Viam, from the same machine that you intend to upload your module from.
+## Understanding modules and models
-{{< readfile "/static/include/how-to/install-cli.md" >}}
+Before you start, it's important to understand the terminology:
-Authenticate your CLI session with Viam using one of the following options:
+**Module** = A package that contains one or more models
+- Think of it like a software package (npm package, Python package)
+- Example: `hello-world` module
+- The distribution unit you upload to the registry
-{{< readfile "/static/include/how-to/auth-cli.md" >}}
-{{< /expand >}}
+**Model** = A specific implementation of a Viam component API
+- A model is the "blueprint" for a type of hardware
+- Each model implements exactly one API (Camera, Sensor, Motor, etc.)
+- Identified by a {{< glossary_tooltip term_id="model-namespace-triplet" text="model namespace triplet" >}} like `exampleorg:hello-world:hello-camera`
+- One module can contain multiple models
-{{% expand "A running machine connected to Viam" %}}
+**Component** = An instance of a model configured on a machine
+- When users add your hardware to their machine, they create a component using your model
+- Multiple components can use the same model (e.g., if someone has two cameras of the same type)
-You can write a module without a machine, but to test your module you'll need a [machine](/operate/install/setup/).
-
-{{% snippet "setup.md" %}}
-
-{{% /expand%}}
-
-{{< expand "For Python developers: Use Python 3.11+" >}}
-
-If you plan to write your module using Python, you need Python 3.11 or newer installed on your computer to use the code generation tool in this guide.
-
-You can check by running `python3 --version` or `python --version` in your terminal.
-
-{{< /expand >}}
-
-## Preparation
-
-While not required, we recommend starting by writing a test script to check that you can connect to and control your hardware from your computer, perhaps using the manufacturer's API or other low-level code.
-
-**Example module:** For the example module, the test script will open an image in the same directory and print a random number.
-
-{{< tabs >}}
-{{% tab name="Python" %}}
-
-```python {class="line-numbers linkable-line-numbers" data-start="1" }
-import random
-from PIL import Image
-
-# Open an image
-img = Image.open("example.png")
-img.show()
-
-# Return a random number
-random_number = random.random()
-print(random_number)
-```
-
-{{% /tab %}}
-{{% tab name="Go" %}}
-
-```go {class="line-numbers linkable-line-numbers" data-start="1" }
-package main
-
-import (
- "fmt"
- "math/rand"
- "os"
-)
-
-func main() {
- // Open an image
- imgFile, err := os.Open("example.png")
- if err != nil {
- fmt.Printf("Error opening image file: %v\n", err)
- return
- }
- defer imgFile.Close()
- imgByte, err := os.ReadFile("example.png")
- fmt.Printf("Image file type: %T\n", imgByte)
- if err != nil {
- fmt.Printf("Error reading image file: %v\n", err)
- return
- }
-
- // Return a random number
- number := rand.Float64()
- fmt.Printf("Random number: %f\n", number)
-}
-```
-
-{{% /tab %}}
-{{< /tabs >}}
-
-## Choose an API
-
-You can think of a module as a packaged wrapper around a script.
-The module takes the functionality of the script and maps it to a standardized API for use within the Viam ecosystem.
-
-Review the available [component APIs](/dev/reference/apis/#component-apis) and choose the one whose methods map most closely to the functionality you need.
-
-If you need a method that is not in your chosen API, you can use the flexible `DoCommand` (which is built into all component APIs) to create custom commands.
-See [Run control logic](/operate/modules/control-logic/) for more information.
-
-**Example module:** To choose the Viam [APIs](/dev/reference/apis/#component-apis) that make sense for your module, think about the functionality you want to implement.
-You need a way to return an image and you need a way to return a number.
-
-If you look at the [camera API](/dev/reference/apis/components/camera/), you can see the `GetImages` method, which returns images from the camera.
-
-The camera API also has a few other methods.
-You do not need to fully implement all the methods of an API.
-For example, this camera does not use point cloud data, so for methods like `GetPointCloud` it will return an "unimplemented" error.
-
-The [sensor API](/dev/reference/apis/components/sensor/) includes the `GetReadings` method.
-You can return the random number with that.
-
-Note that the camera API can't return a number and the sensor API can't return an image.
-Each model can implement only one API, but your module can contain multiple modular resources.
-Therefore it is best to make two modular resources: a camera to return the image and a sensor to return a random number.
-
-## Write your module
-
-### Generate stub files
-
-Use the [Viam CLI](/dev/tools/cli/) to generate template files for your module.
-You can work on the code for your module either on the device where you are running `viam-server` or on another computer.
-
-{{< table >}}
-{{% tablestep start=1 %}}
-
-Run the `module generate` command in your terminal:
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module generate
-```
-
-{{< expand "Click for more details about each prompt" >}}
-
-
-| Prompt | Description |
-| -------| ----------- |
-| Module name | Choose a name that describes the set of {{< glossary_tooltip term_id="resource" text="resources" >}} it supports. |
-| Language | Choose the programming language for the module. The CLI supports `Python` and `Golang`. |
-| Visibility | Choose `Private` to share only with your organization, or `Public` to share publicly with all organizations. If you are testing, choose `Private`. |
-| Namespace/Organization ID | Navigate to your organization settings through the menu in the upper-right corner of the page. Find the **Public namespace** (or create one if you haven't already) and copy that string. If you use the organization ID, you must still create a public namespace first if you wish to share the module publicly. |
-| Resource to add to the module (API) | The [component API](/dev/reference/apis/#component-apis) your module will implement. See [Choose an API](#choose-an-api) for more information. |
-| Model name | Name your component model based on what it supports, for example, if it supports a model of ultrasonic sensor called "XYZ Sensor 1234" you could call your model `xyz_1234` or similar. Must be all-lowercase and use only alphanumeric characters (`a-z` and `0-9`), hyphens (`-`), and underscores (`_`). |
-| Enable cloud build | If you select `Yes` (recommended) and push the generated files (including the .github folder) and create a release of the format `X.X.X`, the module will build for [all architectures specified in the meta.json build file](/operate/modules/advanced/metajson/). You can select `No` if you want to always build the module yourself before uploading it. For more information see [Update and manage modules](/operate/modules/advanced/manage-modules/). |
-| Register module | Select `Yes` unless you are creating a local-only module for testing purposes and do not intend to upload it. Registering a module makes its name and metadata appear in the registry; uploading the actual code that powers the module is a separate step. If you decline to register the module at this point, you can run [`viam module create`](/dev/tools/cli/#module) to register it later. |
-
-{{% /expand %}}
-
-
-
-**Example module**: To build an example module that contains a camera model, use the following command:
-
-{{< tabs >}}
-{{% tab name="Python" %}}
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module generate --language python --model-name hello-camera \
- --name hello-world --resource-subtype=camera --public false \
- --enable-cloud true
-```
-
-{{% /tab %}}
-{{% tab name="Go" %}}
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module generate --language go --model-name hello-camera \
- --name hello-world --resource-subtype=camera --public false \
- --enable-cloud true
-```
-
-{{% /tab %}}
-{{< /tabs >}}
-
-The CLI only supports generating code for one model at a time.
-You can add the model for the sensor in a later step in [Creating multiple models within one module](/operate/modules/support-hardware/#creating-multiple-models-within-one-module).
-
-{{% /tablestep %}}
-{{% tablestep %}}
-
-The generator creates a directory containing stub files for your modular component.
-In the next section, you'll customize some of the generated files to support your camera.
-
-**Example module**: For the example module, the file structure is:
-
-{{< tabs >}}
-{{% tab name="Python" %}}
-
-```treeview
-hello-world/
-└── src/
-| ├── models/
-| | └── hello_camera.py
-| └── main.py
-└── README.md
-└── _hello-world_hello-camera.md
-└── build.sh
-└── meta.json
-└── requirements.txt
-└── run.sh
-└── setup.sh
-```
-
-If you want to understand the module structure, here's what each file does:
-
-- **README.md**: Documentation template that gets uploaded to the registry when you upload the module.
-- **org-id_hello-world_hello-camera.md**: Documentation template for the model that gets uploaded to the registry when you upload the module.
-- **meta.json**: Module metadata that gets uploaded to the registry when you upload the module.
-- **main.py** and **hello_camera.py**: Core code that registers the module and resource and provides the model implementation.
-- **setup.sh** and **requirements.txt**: Setup script that creates a virtual environment and installs the dependencies listed in requirements.txt.
-- **build.sh**: Build script that packages the code for upload.
-- **run.sh**: Script that runs setup.sh and then executes the module from main.py.
-
-{{% /tab %}}
-{{% tab name="Go" %}}
-
-```treeview
-hello-world/
-└── cmd/
-| ├── cli/
-| | └── main.go
-| └── module/
-| └── main.go
-└── Makefile
-└── README.md
-└── _hello-world_hello-camera.md
-└── go.mod
-└── module.go
-└── meta.json
-```
-
-If you want to understand the module structure, here's what each file does:
-
-- **README.md**: Documentation template that gets uploaded to the registry when you upload the module.
-- **org-id_hello-world_hello-camera.md**: Documentation template for the model that gets uploaded to the registry when you upload the module.
-- **meta.json**: Module metadata that gets uploaded to the registry when you upload the module.
-- **module/main.go and module.go**: Core code that registers the module and resource and provides the model implementation.
-- **cli/main.go**: You can run this file to test the model you are creating (`go run ./cmd/cli`).
-- **Makefile**: Build and setup commands.
-
-{{% /tab %}}
-{{< /tabs >}}
-
-{{% /tablestep %}}
-{{< /table >}}
-
-### Creating multiple models within one module
-
-Some of the code you generated for your first modular resource is shared across the module no matter how many modular resource models it supports.
-Some of the code you generated is resource-specific.
-
-If you have multiple modular resources that are related, you can put them all into the same module.
-
-For convenience, we recommend running the module generator again from within the first module's directory, generating an unregistered module, and copying the resource-specific code from it.
-
-**Example module**: Change directory into the first module's directory:
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-cd hello-world
-```
-
-Run the following command from within the first module's directory to generate temporary code you can copy from.
-Do not register this module.
-
-{{< tabs >}}
-{{% tab name="Python" %}}
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module generate --language python --model-name hello-sensor \
- --name hello-world --resource-subtype=sensor --public false \
- --enable-cloud true
-```
-
-Click on each tab to see how the file should change to add the sensor-specific code:
-
-{{< tabs >}}
-{{% tab name="hello_sensor.py" %}}
-
-Move the generated hello-world/hello-world/src/models/hello_sensor.py file to hello-world/src/models/.
-
-{{% /tab %}}
-{{% tab name="main.py" %}}
-
-Open the hello-world/src/main.py file and add `HelloSensor` to the list of imports so you have:
-
-```python {class="line-numbers linkable-line-numbers" data-line="6, 9"}
-import asyncio
-
-from viam.module.module import Module
-try:
- from models.hello_camera import HelloCamera
- from models.hello_sensor import HelloSensor
-except ModuleNotFoundError: # when running as local module with run.sh
- from .models.hello_camera import HelloCamera
- from .models.hello_sensor import HelloSensor
-
-if __name__ == '__main__':
- asyncio.run(Module.run_from_registry())
-```
-
-Save the file.
-
-{{% /tab %}}
-{{% tab name="org-id_hello-world_hello-sensor.md" %}}
-
-Move the generated hello-world/hello-world/org-id_hello-world_hello-sensor.md file to hello-world/org-id_hello-world_hello-sensor.md.
-
-{{% /tab %}}
-{{% tab name="meta.json" %}}
-
-Open hello-world/meta.json and edit the `description` to include both models.
-
-```json {class="line-numbers linkable-line-numbers" data-line="6,13-19"}
-{
- "$schema": "https://dl.viam.dev/module.schema.json",
- "module_id": "exampleorg:hello-world",
- "visibility": "private",
- "url": "",
- "description": "Example camera and sensor components: hello-camera and hello-sensor",
- "applications": null,
- "markdown_link": "README.md",
- "entrypoint": "./run.sh",
- "first_run": "",
- "build": {
- "build": "./build.sh",
- "setup": "./setup.sh",
- "path": "dist/archive.tar.gz",
- "arch": ["linux/amd64", "linux/arm64", "darwin/arm64", "windows/amd64"]
- }
-}
-```
-
-Save the file.
-
-{{% /tab %}}
-{{< /tabs >}}
-
-{{% /tab %}}
-{{% tab name="Go" %}}
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module generate --language go --model-name hello-sensor \
- --name hello-world --resource-subtype=sensor --public false \
- --enable-cloud true
-```
-
-Click on each tab to see how the file should change to add the sensor-specific code:
-
-{{< tabs >}}
-{{% tab name="hello-camera.go" %}}
-
-In the initial module, change the name of hello-world/module.go to hello-camera.go.
-
-{{% /tab %}}
-{{% tab name="hello-sensor.go" %}}
-
-Move and rename hello-world/hello-world/module.go to hello-world/hello-sensor.go.
-
-{{% /tab %}}
-{{% tab name="module/main.go" %}}
-
-Open hello-world/cmd/module/main.go.
-This file must add resource imports and register the module's models:
-
-```go {class="line-numbers linkable-line-numbers" data-start="1" data-line="8, 13-16"}
-package main
-
-import (
- "helloworld"
- "go.viam.com/rdk/module"
- "go.viam.com/rdk/resource"
- camera "go.viam.com/rdk/components/camera"
- sensor "go.viam.com/rdk/components/sensor"
-)
-
-func main() {
- // ModularMain can take multiple APIModel arguments, if your module implements multiple models.
- module.ModularMain(
- resource.APIModel{ camera.API, helloworld.HelloCamera},
- resource.APIModel{ sensor.API, helloworld.HelloSensor},
- )
-}
-```
-
-Save the file.
-
-{{% /tab %}}
-{{% tab name="org-id_hello-world_hello-sensor.md" %}}
-
-Move the generated hello-world/hello-world/org-id_hello-world_hello-sensor.md file to hello-world/org-id_hello-world_hello-sensor.md.
-
-{{% /tab %}}
-{{% tab name="meta.json" %}}
-
-Open hello-world/meta.json and edit the `description` to include both models.
-
-```json {class="line-numbers linkable-line-numbers" data-line="6,13-19"}
-{
- "$schema": "https://dl.viam.dev/module.schema.json",
- "module_id": "exampleorg:hello-world",
- "visibility": "private",
- "url": "",
- "description": "Example camera and sensor components: hello-camera and hello-sensor",
- "applications": null,
- "markdown_link": "README.md",
- "entrypoint": "bin/hello-world",
- "first_run": "",
- "build": {
- "build": "make module.tar.gz",
- "setup": "make setup",
- "path": "module.tar.gz",
- "arch": ["linux/amd64", "linux/arm64", "darwin/arm64", "windows/amd64"]
- }
-}
-```
-
-Save the file.
-
-{{% /tab %}}
-{{< /tabs >}}
-
-{{% /tab %}}
-{{< /tabs >}}
-
-You can now delete the temporary hello-world/hello-world directory and all its contents.
-
-### Implement the components
-
-At this point you have a template for your module.
-
-If you want to see example modules, check out the Viam Registry.
-Many modules have a linked GitHub repo, where you can see the module's code.
-When logged in, you can also download the module's source code to inspect it.
-
-{{< tabs >}}
-{{% tab name="Python" %}}
-
-Generally you will add your custom logic in these files:
-
-
-| File | Description |
-| ---- | ----------- |
-| /src/models/<model-name>.py | Set up the configuration options for the model and implement the API methods for the model. |
-| `setup.sh` and `run.sh` | Add any logic for installing or running other software for your module. |
-| `requirements.txt` | Add any Python packages that are required for your module. They will be installed by `setup.sh`. |
-
-
-
-{{% /tab %}}
-{{% tab name="Go" %}}
-
-Generally you will add your custom logic in these files:
-
-
-| File | Description |
-| ---- | ----------- |
-| Model file (for example `hello-camera.go`) | Implement the API methods for the model. |
-
-{{% /tab %}}
-{{< /tabs >}}
-
-**Example module**: You can view complete example code in the [hello-world-module repository on GitHub](https://github.com/viam-labs/hello-world-module/tree/main).
-
-#### Set up model configuration options
-
-Many resource models have configuration options that allow you to specify options such as:
-
-- A file path from which to access data
-- A pin to which a device is wired
-- An optional signal frequency to override a default value
-- The name of _another_ resource you wish to use in the model
-
-Model configuration happens in two steps:
-
-{{< table >}}
-{{% tablestep start=1 %}}
-**Validation**
-
-The validation step serves two purposes:
-
-- Confirm that the model configuration contains all **required attributes** and that these attributes are of the right type.
-- Identify and return a list of names of **required resources** and a list of names of **optional resources**.
- `viam-server` will pass these resources to the next step as dependencies.
- For more information, see [Module dependencies](/operate/modules/advanced/dependencies/).
-
-**Example module**: Imagine how a user might configure the finished camera model.
-Since the camera model returns an image at a provided path, the configuration must contain a variable to pass in the file path.
-
-```json
-{
- "image_path": "/path/to/file"
-}
-```
-
-{{< tabs >}}
-{{% tab name="Python" %}}
-
-In /src/models/<model-name>.py, edit the `validate_config` function to:
-
-```python {class="line-numbers linkable-line-numbers" data-start="38" data-line="5-10" }
- @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 [], []
-```
-
-{{% /tab %}}
-{{% tab name="Go" %}}
-
-In hello-world/hello-camera.go edit the `Validate` function to:
-
-```go {class="line-numbers linkable-line-numbers" data-start="51" data-line="2-10" }
-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 following import at the top of hello-world/hello-camera.go:
-
-```go {class="line-numbers linkable-line-numbers" data-start="7"}
-"reflect"
-```
-
-{{% /tab %}}
-{{< /tabs >}}
-
-For the sensor model, you do not need to edit any of the validation or configuration methods because the sensor has no configurable attributes.
-
-{{% /tablestep %}}
-{{% tablestep %}}
-**Reconfiguration**
-
-`viam-server` calls the `reconfigure` method when the user adds the model or changes its configuration.
-
-The reconfiguration step serves two purposes:
-
-- Use the configuration attributes and dependencies to set attributes on the model for usage within the API methods.
-- Obtain access to dependencies.
- For information on how to use dependencies, see [Module dependencies](/operate/modules/advanced/dependencies/).
-
-**Example module**: For the camera model, the reconfigure method serves to set the image path for use in API methods.
-
-{{< tabs >}}
-{{% tab name="Python" %}}
-
-1. Open /src/models/hello_camera.py.
-
-2. Edit the `reconfigure` function to:
-
- ```python {class="line-numbers" data-start="51" data-line="4-5"}
- 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)
- ```
-
-3. Add the following import to the top of the file:
-
- ```python {class="line-numbers" data-start="1"}
- from viam.utils import struct_to_dict
- ```
-
-{{% /tab %}}
-{{% tab name="Go" %}}
-
-1. Open hello-world/hello-camera.go.
-
-1. Add `imagePath = ""` to the global variables so you have the following:
-
- ```go {class="line-numbers linkable-line-numbers" data-line="22" data-start="18" data-line-offset="19"}
- var (
- HelloCamera = resource.NewModel("exampleorg", "hello-world", "hello-camera")
- errUnimplemented = errors.New("unimplemented")
- imagePath = ""
- )
- ```
-
-1. Edit the `type Config struct` definition, replacing the comments with the following:
-
- ```go {class="line-numbers" data-start="32"}
- 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.
-
-{{< expand "Need to maintain state? Click here." >}}
-The `resource.AlwaysRebuild` parameter in the `Config` struct causes `viam-server` to fully rebuild the resource each time the user changes the configuration.
-
-If you need to maintain the state of the resource, for example if you are implementing a board and need to keep the software PWM loops running, you can implement this function so `viam-server` updates the configuration without rebuilding the resource from scratch.
-In this case, your `Reconfigure` function should do the following:
-
-- If you assigned any configuration attributes to global variables, get the values from the latest `config` object and update the values of the global variables.
-- Assign default values as necessary to any optional attributes if the user hasn't configured them.
-
-If you create a `Reconfigure` function, you must also edit the constructor to explicitly call `Reconfigure`.
-
-For an example that implements the `Reconfigure` method, see [mybase.go on GitHub](https://github.com/viamrobotics/rdk/blob/main/examples/customresources/models/mybase/mybase.go).
-
-{{< /expand >}}
-
-{{% hiddencontent %}}
-`resource.AlwaysRebuild` provides an implementation of `Reconfigure` that returns a `NewMustRebuild` error.
-This error doesn't exist in the other SDKs, so `AlwaysRebuild` is not supported in those SDKs.
-{{% /hiddencontent %}}
-
-{{% /tab %}}
-{{< /tabs >}}
-
-{{% /tablestep %}}
-{{< /table >}}
-
-#### Implement API methods
-
-Depending on the component API you are implementing, you can implement different API methods.
-
-{{< tabs >}}
-{{% tab name="Python" %}}
-
-For each API method you want to implement, replace the body of the method with your relevant logic.
-Make sure you return the correct type in accordance with the function's return signature.
-You can find details about the return types at [python.viam.dev](https://python.viam.dev/autoapi/viam/components/index.html).
-
-**Example module:** Implement the camera API and the sensor API:
-
-{{< table >}}
-{{< tablestep start=1 >}}
-
-The module generator created a stub for the `get_images()` function we want to implement in hello-world/src/models/hello_camera.py.
-
-You need to replace `raise NotImplementedError()` with code to implement the method:
-
-```python {class="line-numbers linkable-line-numbers" data-start="74" data-line="9-13" }
- 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 the following import to the top of the file:
-
-```python {class="line-numbers" data-start="1"}
-from viam.media.utils.pil import pil_to_viam_image
-from viam.media.video import CameraMimeType
-from PIL import Image
-```
-
-Save the file.
-
-{{% /tablestep %}}
-{{< tablestep >}}
-
-Leave the rest of the camera API methods unimplemented.
-They do not apply to this camera.
-
-{{% /tablestep %}}
-{{< tablestep >}}
-
-Open requirements.txt.
-Add the following line:
-
-```text
-Pillow
-```
-
-{{% /tablestep %}}
-{{< tablestep >}}
-
-Next, implement the sensor API.
-The module generator created a stub for the `get_readings()` function we want to implement in hello-world/src/models/hello_sensor.py.
-
-Replace `raise NotImplementedError()` with code to implement the method:
-
-```python {class="line-numbers linkable-line-numbers" data-start="63" data-line="8-11" }
- async def get_readings(
- self,
- *,
- extra: Optional[Mapping[str, Any]] = None,
- timeout: Optional[float] = None,
- **kwargs
- ) -> Mapping[str, SensorReading]:
- number = random.random()
- return {
- "random_number": number
- }
-```
-
-Add the following import to the top of the file:
-
-```python {class="line-numbers" data-start="1"}
-import random
-```
-
-Save the file.
-
-{{% /tablestep %}}
-{{< tablestep >}}
-
-Leave the rest of the sensor API methods unimplemented.
-
-{{% /tablestep %}}
-{{< /table >}}
-
-{{% hiddencontent %}}
-
-You may see examples in registry modules that use a different pattern from what the generator creates.
-For example, some older example modules define `async def main()` inside main.py.
-We recommend using the pattern the generator follows:
-
-```python {class="line-numbers linkable-line-numbers"}
-import asyncio
-from viam.module.module import Module
-try:
- from models.hello_camera import HelloCamera
-except ModuleNotFoundError:
- # when running as local module with run.sh
- from .models.hello_camera import HelloCamera
-
-if __name__ == '__main__':
- asyncio.run(Module.run_from_registry())
-```
-
-A previous version of the CLI module generator created `__init__.py` files, but now uses a different module structure.
-We recommend using what the current generator creates rather than old examples that use `__init__.py` files.
-
-{{% /hiddencontent %}}
-
-{{% /tab %}}
-{{% tab name="Go" %}}
-
-For each API method you want to implement, replace the body of the method with your relevant logic.
-Make sure you return the correct type in accordance with the function's return signature.
-You can find details about the return types at [go.viam.com/rdk/components](https://pkg.go.dev/go.viam.com/rdk/components).
-
-**Example module:** Implement the camera API and the sensor API:
-
-{{< table >}}
-{{< tablestep start=1 >}}
-
-The module generator created a stub for the `Images` function we want to implement in hello-world/hello-camera.go.
-
-You need to replace `panic("not implemented")` with code to implement the method:
-
-```go {class="line-numbers linkable-line-numbers" data-start="111" }
-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
-}
-```
-
-{{% /tablestep %}}
-{{% tablestep %}}
-
-Add the following import at the top of hello-world/hello-camera.go:
-
-```go {class="line-numbers linkable-line-numbers" data-start="7"}
-"os"
-```
-
-Save the file.
-
-{{% /tablestep %}}
-{{< tablestep >}}
-
-Leave the rest of the camera API methods unimplemented.
-They do not apply to this camera.
-
-{{% /tablestep %}}
-{{< tablestep >}}
-
-Next, implement the sensor API.
-The module generator created a stub for the `Readings()` function we want to implement in hello-world/hello-sensor.go.
-
-Replace `panic("not implemented")` with code to implement the method:
-
-```go {class="line-numbers linkable-line-numbers" data-start="92" data-line="8-11" }
-func (s *helloWorldHelloSensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) {
- number := rand.Float64()
- return map[string]interface{}{
- "random_number": number,
- }, nil
-}
-```
-
-{{% /tablestep %}}
-{{< tablestep >}}
-
-Add the following import to the list of imports at the top of hello-world/hello-sensor.go:
-
-```go {class="line-numbers linkable-line-numbers" data-start="7"}
-"math/rand"
-```
-
-{{% /tablestep %}}
-{{< tablestep >}}
-
-Since `errUnimplemented` and `Config` are defined in hello-camera.go, you need to change hello-sensor.go to avoid redeclaring them:
-
-In hello-sensor.go:
-
-- Delete the `"errors"` import.
-- Search for and delete the line `errUnimplemented = errors.New("unimplemented")`.
-- Search for `type Config struct {` and change it to `type sensorConfig struct {`.
-- Search for all instances of `*Config` in hello-sensor.go and change them to `*sensorConfig`.
-
-{{% /tablestep %}}
-{{< tablestep >}}
-
-Leave the rest of the sensor API methods unimplemented.
-
-Save the file.
-
-{{% /tablestep %}}
-
-{{< /table >}}
-
-{{% /tab %}}
-{{< /tabs >}}
-
-## Test your module locally
-
-You can test your module locally before uploading it to the [registry](https://app.viam.com/registry).
-
-### Add module to machine
-
-To get your module onto your machine, hot reloading builds and packages it and then uses the shell service to copy it to the machine for testing.
-If your files are already on the machine, you can add the module manually instead.
-
-{{% hiddencontent %}}
-Hot reloading is the preferred solution for cross-compilation. We recommend using hot reloading rather than cross-compilation tools like Canon.
-{{% /hiddencontent %}}
-
-{{< tabs >}}
-{{% tab name="Hot reloading (recommended)" %}}
-
-Run the following command to build the module and add it to your machine:
-
-{{< tabs >}}
-{{% tab name="Same device" %}}
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module reload-local --cloud-config /path/to/viam.json
-```
-
-{{% /tab %}}
-{{% tab name="Other device" %}}
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module reload --part-id 123abc45-1234-432c-aabc-z1y111x23a00
-```
-
-{{% /tab %}}
-{{< /tabs >}}
-
-For more information, see the [`viam module` documentation](/dev/tools/cli/#module).
-
-{{< expand "Reload troubleshooting" >}}
-
-- `Error: Could not connect to machine part: context deadline exceeded; context deadline exceeded; mDNS query failed to find a candidate`
-
- Try specifying the `--part-id`, which you can find by clicking the **Live** indicator on your machine's page and clicking **Part ID**.
-
-- `Error: Rpc error: code = Unknown desc = stat /root/.viam/packages-local: no such file or directory`
-
- Try specifying the `--home` directory, for example `/Users/yourname/` on macOS.
-
-- `Error: Error while refreshing token, logging out. Please log in again`
-
- Run `viam login` to reauthenticate the CLI.
-
-### Try using a different command
-
-If you are still having problems with the `reload` command, you can use a different, slower method of rebuilding and then restarting the module.
-Run the following command to rebuild your module:
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module build local
-```
-
-Then restart it on your machine's **CONFIGURE** tab.
-In the upper-right corner of the module's card, click the **...** menu, then click **Restart**.
-
-{{}}
-
-{{< /expand >}}
-
-{{< alert title="Refresh" color="note" >}}
-
-You may need to refresh your machine page for your module to show up.
+**In this tutorial:**
+- **Module**: `hello-world` (the package)
+- **Models**:
+ - `hello-camera` (implements Camera API)
+ - `hello-sensor` (implements Sensor API)
+- **Components**: Users create `camera-1`, `camera-2`, `sensor-1`, etc. using these models
+{{< alert title="Why multiple models?" color="note" >}}
+Each model can only implement one API. Since our example hardware has two capabilities (returning images and returning numbers), and the Camera API can't return numbers while the Sensor API can't return images, we need two separate models in the same module.
{{< /alert >}}
-{{% /tab %}}
-{{% tab name="Manual" %}}
+## Tutorial overview
-Navigate to your machine's **CONFIGURE** page.
+This tutorial is divided into 5 parts:
-{{< tabs >}}
-{{% tab name="Python" %}}
+| Part | Time | What you'll do |
+| ---- | ---- | -------------- |
+| [**Part 1: Prerequisites and setup**](part-1-prerequisites-setup/) | 15-20 min | Install CLI, set up your development environment, understand module architecture |
+| [**Part 2: Choose an API and generate code**](part-2-choose-api-generate/) | 15 min | Select the right API for your hardware, generate module structure with CLI |
+| [**Part 3: Implement your module**](part-3-implement-single-model/) | 30-40 min | Implement configuration validation, reconfiguration, and API methods |
+| [**Part 4: Test your module locally**](part-4-test-locally/) | 20 min | Use hot reload to test your module on a real machine |
+| [**Part 5: Multiple models**](part-5-multiple-models/) | 20-25 min | **(Advanced)** Add a second model to your module |
-Click the **+** button, select **Local module**, then again select **Local module**.
+**Total time:** ~2 hours | **Difficulty:** Intermediate
-Enter the path to the automatically-generated run.sh script.
-Click **Create**.
-For local modules, `viam-server` uses this path to start the module.
+### Prerequisites
-**Example module**:
-For the `hello-world` module, the path should resemble `/home/yourname/hello-world/run.sh` on Linux, or `/Users/yourname/hello-world/run.sh` on macOS.
+Before you begin, make sure you have:
-Save the config.
+- **Viam CLI** installed and authenticated
+- **Python 3.11+** or **Go 1.20+** for development
+- **(Recommended)** A test machine running `viam-server`
-{{% /tab %}}
-{{% tab name="Go" %}}
+See [Part 1](part-1-prerequisites-setup/) for detailed setup instructions.
-From within the module directory, compile your module [with the `module build` command](/dev/tools/cli/#using-the-build-subcommand) into a single executable:
-
-```sh {class="command-line" data-prompt="$" data-output="5-10"}
-viam module build local
-```
+## When to create a module
-Click the **+** button, select **Local module**, then again select **Local module**.
+Create a module when:
+- Your hardware isn't supported by [existing modules](https://app.viam.com/registry)
+- You need to implement custom logic or algorithms
+- You want to integrate proprietary hardware or APIs
+- You want to share your hardware support with others
-Enter the path to the /bin/<module-name> executable.
-For local modules, `viam-server` uses this path to start the module.
+## Module creation workflow
-**Example module**:
-For the `hello-world` module, the path should resemble `/home/yourname/hello-world/bin/hello-world`.
-
-Click **Create**.
-
-Save the config.
-
-{{% /tab %}}
-{{< /tabs >}}
-
-{{% /tab %}}
-{{< /tabs >}}
-
-{{< table >}}
-{{< /table >}}
-
-### Add local model
-
-{{< table >}}
-{{% tablestep start=1 %}}
-**Configure the model provided by your module**
-
-On your machine's **CONFIGURE** page, click **+**, click **Local module**, then click **Local component** or **Local service**.
-
-Select or enter the {{< glossary_tooltip term_id="model-namespace-triplet" text="model namespace triplet" >}}, for example `exampleorg:hello-world:hello-camera`.
-You can find the triplet in the `model` field of your meta.json file.
-
-Select the **Type** corresponding to the API you implemented.
-
-Enter a **Name** such as `camera-1`.
-Click **Create**.
-
-{{% /tablestep %}}
-{{% tablestep %}}
-**Configure attributes**
-
-When you add a new component or service, a panel appears for it on the **CONFIGURE** tab.
-If your model has required or optional attributes, configure them in the configuration field by adding them inside the `{}` object.
-
-**Example module**: For the camera model, add the `image_path` attribute by replacing `{}` with:
-
-```json {class="line-numbers linkable-line-numbers"}
-{
- "image_path": ""
-}
+```mermaid
+graph TD
+ A[Choose API] --> B[Generate Code]
+ B --> C[Implement]
+ C --> D[Test Locally]
+ D --> E[Deploy]
+ E --> F[Share]
```
-{{% /tablestep %}}
-{{% tablestep %}}
-Save the config and wait a few seconds for it to apply.
-
-Then click the **TEST** section of the camera's configuration card.
-If there are errors you will see them on the configuration panel and on the **LOGS** tab.
-
-{{% /tablestep %}}
-{{% tablestep %}}
-**Test the component**
-
-Click the **TEST** bar at the bottom of your modular component configuration, and check whether it works as expected.
-
-**Example module**: For the camera model, the test panel should show the image:
-
-{{}}
-
-If you also implemented the sensor model, add and test it the same way.
+1. **Choose an API**: Match your hardware to a Viam component API
+2. **Generate code**: Use the CLI to create module structure
+3. **Implement**: Write code to control your hardware
+4. **Test locally**: Verify functionality on your machine
+5. **Deploy**: Upload to the Viam registry
+6. **Share**: Make it public or keep it private
-{{% /tablestep %}}
-{{% tablestep %}}
-**Iterate**
+## Alternative guides
-If your component works, you're almost ready to share your module by uploading it to the registry.
+**For different hardware platforms:**
+- **Microcontrollers (ESP32)**: See [Modules for ESP32](/operate/modules/advanced/micro-module/)
+- **C++ development**: See [C++ examples on GitHub](https://github.com/viamrobotics/viam-cpp-sdk/tree/main/src/viam/examples/)
-Each time you make changes to the local module code, you must update the module on the machine:
+**For different module types:**
+- **Control logic modules** (no hardware): See [Run control logic](/operate/modules/control-logic/)
-{{< tabs >}}
-{{% tab name="Hot reloading (recommended)" %}}
+## Get started
-Run the reload command again to rebuild and restart your module:
+Ready to create your first module?
-{{< tabs >}}
-{{% tab name="Same device" %}}
+**[Begin Part 1: Prerequisites and setup →](part-1-prerequisites-setup/)**
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module reload-local --cloud-config /path/to/viam.json
-```
-
-{{% /tab %}}
-{{% tab name="Other device" %}}
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module reload --part-id 123abc45-1234-432c-aabc-z1y111x23a00
-```
-
-{{% /tab %}}
-{{< /tabs >}}
-
-Your machine may already have a previously published version of the module you are iterating on in its configuration.
-If so you can toggle **Hot Reloading** on and off in the Viam web UI.
-When toggled on, the machine uses the module that you are developing.
-When toggled off, the machine uses the configured registry version.
-
-{{% /tab %}}
-{{% tab name="Manual" %}}
-
-{{< tabs >}}
-{{% tab name="Python" %}}
-
-As you iterate, save the code changes, then restart the module in your machine's **CONFIGURE** tab:
-In the upper-right corner of the module's card, click **...** menu, then click **Restart**.
-
-{{}}
-
-{{% /tab %}}
-{{% tab name="Go" %}}
-
-Run the following command to rebuild your module:
-
-```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
-viam module build local
-```
-
-Then restart it in your machine's **CONFIGURE** tab.
-In the upper-right corner of the module's card, click **...** menu, then click **Restart**.
-
-{{}}
-
-{{% /tab %}}
-{{< /tabs >}}
-
-{{% /tab %}}
-
-{{< /tabs >}}
+---
-{{% /tablestep %}}
-{{< /table >}}
+## After you finish
-## Next steps
+Once you complete this tutorial:
-Once you have thoroughly tested your module, continue to [package and deploy](/operate/modules/deploy-module/) it.
+1. **Deploy your module**: [Package and deploy to the registry](/operate/modules/deploy-module/)
+2. **Manage your modules**: [Update and manage modules](/operate/modules/advanced/manage-modules/)
+3. **Learn advanced topics**:
+ - [Module dependencies](/operate/modules/advanced/dependencies/)
+ - [Module configuration](/operate/modules/advanced/module-configuration/)
+ - [Logging in modules](/operate/modules/advanced/logging/)
+ - [meta.json reference](/operate/modules/advanced/metajson/)
diff --git a/docs/operate/modules/support-hardware/part-1-prerequisites-setup.md b/docs/operate/modules/support-hardware/part-1-prerequisites-setup.md
new file mode 100644
index 0000000000..379107db00
--- /dev/null
+++ b/docs/operate/modules/support-hardware/part-1-prerequisites-setup.md
@@ -0,0 +1,209 @@
+---
+title: "Part 1: Prerequisites and setup"
+linkTitle: "Part 1: Setup"
+weight: 31
+layout: "docs"
+type: "docs"
+description: "Set up your development environment and prepare to create a custom Viam module."
+---
+
+**Part 1 of 5** | ⏱️ 15-20 minutes
+
+In this tutorial series, you'll learn how to create a custom {{< glossary_tooltip term_id="module" text="module" >}} to add support for hardware that isn't already supported by existing registry modules.
+
+## What you'll do in this part
+
+- Install and authenticate the Viam CLI
+- Set up a test machine (optional but recommended)
+- Verify your development environment (Python 3.11+ or Go 1.20+)
+- Write a test script to verify hardware connectivity
+- Understand the module architecture
+
+## What you'll build
+
+Throughout this tutorial series, you'll create a **hello-world module** that demonstrates two common module capabilities:
+
+1. **Camera model**: Returns an image from a configured file path
+2. **Sensor model**: Returns a random number reading
+
+This example shows how to implement multiple {{< glossary_tooltip term_id="resource" text="resources" >}} within a single module.
+
+## Prerequisites
+
+Before you begin, make sure you have the following:
+
+### 1. Install and authenticate the Viam CLI
+
+The Viam CLI is required to generate module code and upload modules to the registry.
+
+Install the Viam CLI on your development machine:
+
+{{< readfile "/static/include/how-to/install-cli.md" >}}
+
+**Verify installation:**
+```sh {class="command-line" data-prompt="$"}
+viam version
+```
+
+You should see output like: `viam version 0.x.x`
+
+**Authenticate to Viam:**
+
+{{< readfile "/static/include/how-to/auth-cli.md" >}}
+
+### 2. Set up a machine for testing
+
+While you can develop a module without a machine, you'll need one to test your module.
+
+{{% snippet "setup.md" %}}
+
+**Already have a machine?** Great! You'll configure it in [Part 4](/operate/modules/support-hardware/part-4-test-locally/).
+
+### 3. Development environment
+
+Your module can be written in Python or Go. Choose your preferred language and verify your environment:
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+**Required:** Python 3.11 or newer
+
+Check your version:
+```sh {class="command-line" data-prompt="$"}
+python3 --version
+```
+
+You should see: `Python 3.11.x` or higher
+
+**Need to install Python 3.11+?**
+- **macOS:** `brew install python@3.11`
+- **Ubuntu/Debian:** `sudo apt install python3.11`
+- **Windows:** Download from [python.org](https://python.org)
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+**Required:** Go 1.20 or newer
+
+Check your version:
+```sh {class="command-line" data-prompt="$"}
+go version
+```
+
+You should see: `go version go1.20` or higher
+
+**Need to install Go?**
+Download from [go.dev](https://go.dev/dl/)
+
+{{% /tab %}}
+{{< /tabs >}}
+
+## Write a test script
+
+Before writing a module, it's helpful to write a simple test script to verify you can control your hardware.
+
+For the example module, this script opens an image file and prints a random number.
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+Create a file named `test.py`:
+
+```python {class="line-numbers linkable-line-numbers" data-start="1"}
+import random
+from PIL import Image
+
+# Open an image
+img = Image.open("example.png")
+img.show()
+
+# Return a random number
+random_number = random.random()
+print(random_number)
+```
+
+**Test it:**
+```sh {class="command-line" data-prompt="$"}
+python3 test.py
+```
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+Create a file named `test.go`:
+
+```go {class="line-numbers linkable-line-numbers" data-start="1"}
+package main
+
+import (
+ "fmt"
+ "math/rand"
+ "os"
+)
+
+func main() {
+ // Open an image
+ imgFile, err := os.Open("example.png")
+ if err != nil {
+ fmt.Printf("Error opening image file: %v\n", err)
+ return
+ }
+ defer imgFile.Close()
+ imgByte, err := os.ReadFile("example.png")
+ fmt.Printf("Image file type: %T\n", imgByte)
+ if err != nil {
+ fmt.Printf("Error reading image file: %v\n", err)
+ return
+ }
+
+ // Return a random number
+ number := rand.Float64()
+ fmt.Printf("Random number: %f\n", number)
+}
+```
+
+**Test it:**
+```sh {class="command-line" data-prompt="$"}
+go run test.go
+```
+
+{{% /tab %}}
+{{< /tabs >}}
+
+{{< alert title="Tip" color="tip" >}}
+For your own hardware, replace this test script with code that uses the manufacturer's API or SDK to control your device.
+{{< /alert >}}
+
+## Understanding module architecture
+
+A module is essentially a packaged wrapper around your hardware control code. The module:
+
+1. **Registers** with `viam-server` when it starts
+2. **Validates** user configuration when they add your component
+3. **Implements** standard Viam API methods (like `GetImages` for cameras)
+4. **Communicates** with your hardware using your control code
+
+In [Part 2](/operate/modules/support-hardware/part-2-choose-api-generate/), you'll choose which Viam API best matches your hardware's capabilities.
+
+## What you've accomplished
+
+**Environment ready:**
+- Viam CLI installed and authenticated
+- Machine set up for testing (optional)
+- Development environment verified (Python 3.11+ or Go 1.20+)
+- Test script working
+
+**Understanding:**
+- What modules are and why you'd create one
+- Basic module architecture
+
+## Next steps
+
+Now that your environment is ready, continue to [Part 2: Choose an API and generate code](/operate/modules/support-hardware/part-2-choose-api-generate/) to select the right API for your hardware and generate your module structure.
+
+---
+
+**Tutorial navigation:**
+- **Current:** Part 1: Prerequisites and setup
+- **Next:** [Part 2: Choose an API and generate code →](/operate/modules/support-hardware/part-2-choose-api-generate/)
+- **All parts:** [Module creation tutorial](/operate/modules/support-hardware/)
diff --git a/docs/operate/modules/support-hardware/part-2-choose-api-generate.md b/docs/operate/modules/support-hardware/part-2-choose-api-generate.md
new file mode 100644
index 0000000000..545be50242
--- /dev/null
+++ b/docs/operate/modules/support-hardware/part-2-choose-api-generate.md
@@ -0,0 +1,289 @@
+---
+title: "Part 2: Choose an API and generate code"
+linkTitle: "Part 2: Choose API"
+weight: 32
+layout: "docs"
+type: "docs"
+description: "Select the right Viam API for your hardware and generate your module code structure."
+---
+
+**Part 2 of 5** | ⏱️ 15 minutes
+
+## What you'll do in this part
+
+- Understand how modules map hardware to Viam APIs
+- Choose the right API for your hardware
+- Generate module code using the Viam CLI
+- Understand the generated file structure
+
+## Choose an API
+
+A module wraps your hardware driver in a standardized Viam API. This means:
+- Your hardware works seamlessly with all Viam SDKs
+- You can use Viam services (vision, data capture, etc.)
+- Configuration is consistent across all components
+
+### How to choose
+
+You can think of a module as a packaged wrapper around a script. The module takes the functionality of the script and maps it to a standardized API for use within the Viam ecosystem.
+
+Review the available [component APIs](/dev/reference/apis/#component-apis) and choose the one whose methods map most closely to the functionality you need.
+
+If you need a method that is not in your chosen API, you can use the flexible `DoCommand` (which is built into all component APIs) to create custom commands. See [Run control logic](/operate/modules/control-logic/) for more information.
+
+### Hardware-to-API mapping guide
+
+| Hardware Type | Viam API | When to Use | Example Devices | Key Methods |
+|---------------|----------|-------------|-----------------|-------------|
+| **Image capture** | [Camera](/dev/reference/apis/components/camera/) | Captures still images or video streams | Webcam, Raspberry Pi Camera, RTSP stream, CV pipeline | `GetImages`, `GetPointCloud` |
+| **Measurement sensor** | [Sensor](/dev/reference/apis/components/sensor/) | Reads numeric values or data | Temperature, IMU, distance, air quality, GPS | `GetReadings` |
+| **Single motor** | [Motor](/dev/reference/apis/components/motor/) | Controls one motor independently | DC motor, stepper motor, servo | `SetPower`, `GoTo`, `GetPosition` |
+| **Multi-motor platform** | [Base](/dev/reference/apis/components/base/) | Coordinated control of multiple motors | Rover, mobile robot, wheeled platform | `MoveStraight`, `Spin`, `SetVelocity` |
+| **Robotic arm** | [Arm](/dev/reference/apis/components/arm/) | Multi-joint arm with kinematics | Robot arm, manipulator | `MoveToPosition`, `MoveToJointPositions` |
+| **Other hardware** | [Generic](/dev/reference/apis/components/generic/) | Doesn't fit standard categories | Custom actuators, unique hardware | `DoCommand` only |
+
+### Decision process
+
+**Step 1:** Identify your hardware's primary function
+
+**Step 2:** Check if an existing API provides the methods you need
+- [Full API reference](/dev/reference/apis/#component-apis)
+- Each API has specific methods designed for that type of hardware
+
+**Step 3:** For multiple functions, create multiple models
+- Each model implements one API
+- One module can contain many models
+- You'll learn how to do this in [Part 5](/operate/modules/support-hardware/part-5-multiple-models/)
+
+### Example: The hello-world module
+
+**Hardware capabilities:**
+1. Returns an image from a file path
+2. Returns a random number
+
+**API mapping:**
+1. Image retrieval → [Camera API](/dev/reference/apis/components/camera/) (`GetImages` method)
+2. Number reading → [Sensor API](/dev/reference/apis/components/sensor/) (`GetReadings` method)
+
+**Result:** Two models in one module
+
+{{< alert title="Can't decide?" color="note" >}}
+Start with the Generic API and migrate later if needed. You're not locked into your initial choice.
+{{< /alert >}}
+
+## Generate module code
+
+Use the [Viam CLI](/dev/tools/cli/) to generate template files for your module. You can work on your module either on the device running `viam-server` or on another computer.
+
+### Run the generator
+
+Run the `module generate` command in your terminal:
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+viam module generate
+```
+
+The CLI will prompt you for several configuration options:
+
+{{< expand "Understanding each prompt" >}}
+
+| Prompt | Description | Example |
+| -------| ----------- | ------- |
+| **Module name** | Choose a name that describes the set of {{< glossary_tooltip term_id="resource" text="resources" >}} it supports | `hello-world` |
+| **Language** | Choose the programming language for the module: `Python` or `Golang` | `Python` |
+| **Visibility** | Choose `Private` to share only with your organization, or `Public` to share publicly with all organizations | `Private` (for testing) |
+| **Namespace/Organization ID** | Navigate to your organization settings through the menu in the upper-right corner of the page. Find the **Public namespace** (or create one if you haven't already) and copy that string | `exampleorg` |
+| **Resource to add** | The [component API](/dev/reference/apis/#component-apis) your module will implement | `camera` |
+| **Model name** | Name your component model based on what it supports. Must be all-lowercase and use only alphanumeric characters (`a-z` and `0-9`), hyphens (`-`), and underscores (`_`) | `hello-camera` |
+| **Enable cloud build** | If you select `Yes` (recommended) and push the generated files (including the .github folder) to GitHub and create a release of the format `X.X.X`, the module will build for all architectures | `Yes` |
+| **Register module** | Select `Yes` unless you're creating a local-only module for testing purposes | `Yes` |
+
+{{< /expand >}}
+
+### Example command
+
+For the hello-world module, you can skip the interactive prompts by providing all options on the command line:
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+viam module generate --language python --model-name hello-camera \
+ --name hello-world --resource-subtype=camera --public false \
+ --enable-cloud true
+```
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+viam module generate --language go --model-name hello-camera \
+ --name hello-world --resource-subtype=camera --public false \
+ --enable-cloud true
+```
+
+{{% /tab %}}
+{{< /tabs >}}
+
+{{< alert title="Note" color="note" >}}
+The CLI only supports generating code for one model at a time. You'll add the sensor model in [Part 5](/operate/modules/support-hardware/part-5-multiple-models/).
+{{< /alert >}}
+
+## Understand the generated files
+
+The generator creates a directory containing stub files for your modular component. Here's what you need to know about each file:
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+```treeview
+hello-world/
+└── src/
+| ├── models/
+| | └── hello_camera.py
+| └── main.py
+└── README.md
+└── _hello-world_hello-camera.md
+└── build.sh
+└── meta.json
+└── requirements.txt
+└── run.sh
+└── setup.sh
+```
+
+### 📝 Files you'll actively edit
+
+These are where your main work happens:
+
+**`src/models/hello_camera.py`**
+- Your hardware control logic goes here
+- Implement API methods (`GetImages`, etc.)
+- Add configuration validation
+- **This is your primary workspace**
+
+**`requirements.txt`**
+- List Python package dependencies
+- Installed automatically by `setup.sh`
+- Example: Add `Pillow` for image processing
+
+**`README.md`** and **`_hello-world_hello-camera.md`**
+- Module and model documentation
+- Shows in the Viam registry
+- Helps users configure your module
+
+### ⚙️ Files you'll occasionally edit
+
+**`meta.json`**
+- Module metadata
+- Entrypoint configuration
+- Build settings
+- Edit when: Adding models, changing description, configuring builds
+
+**`src/main.py`**
+- Registers models with viam-server
+- Edit when: Adding new models to existing module
+
+### 🤖 Generated/managed files (usually don't edit)
+
+**`setup.sh`, `build.sh`, `run.sh`**
+- Build and execution scripts
+- Generated by CLI
+- Edit only for custom build requirements
+
+**`.github/workflows/build.yml`**
+- GitHub Actions for cloud build
+- Auto-uploads module on release
+- Edit only to customize CI/CD
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+```treeview
+hello-world/
+└── cmd/
+| ├── cli/
+| | └── main.go
+| └── module/
+| └── main.go
+└── Makefile
+└── README.md
+└── _hello-world_hello-camera.md
+└── go.mod
+└── module.go
+└── meta.json
+```
+
+### 📝 Files you'll actively edit
+
+**`module.go`**
+- Your hardware control logic goes here
+- Implement API methods (`Images`, etc.)
+- Add configuration validation
+- **This is your primary workspace**
+
+**`README.md`** and **`_hello-world_hello-camera.md`**
+- Module and model documentation
+- Shows in the Viam registry
+- Helps users configure your module
+
+### ⚙️ Files you'll occasionally edit
+
+**`meta.json`**
+- Module metadata
+- Entrypoint configuration
+- Build settings
+- Edit when: Adding models, changing description
+
+**`cmd/module/main.go`**
+- Registers models with viam-server
+- Edit when: Adding new models to existing module
+
+### 🤖 Generated/managed files (usually don't edit)
+
+**`cmd/cli/main.go`**
+- Test runner for local development
+- Run with: `go run ./cmd/cli`
+
+**`Makefile`**
+- Build and setup commands
+- Generated by CLI
+
+**`go.mod`**
+- Go module dependencies
+- Managed by Go toolchain
+
+**`.github/workflows/build.yml`**
+- GitHub Actions for cloud build
+- Auto-uploads module on release
+
+{{% /tab %}}
+{{< /tabs >}}
+
+💡 **Getting started tip:** Focus on the main model file (`hello_camera.py` or `module.go`). Everything else can wait until you need it.
+
+## What you've accomplished
+
+✅ **API selected:**
+- Understand how hardware maps to Viam APIs
+- Chosen the right API for your hardware (Camera for the example)
+
+✅ **Module generated:**
+- Created module structure with CLI
+- Understand what each file does
+- Know which files to edit first
+
+✅ **Ready to code:**
+- Have a working module scaffold
+- Ready to implement API methods
+
+## Next steps
+
+Now that you have your module structure, continue to [Part 3: Implement your module](/operate/modules/support-hardware/part-3-implement-single-model/) to write the code that makes your hardware work.
+
+---
+
+**Tutorial navigation:**
+- **Previous:** [← Part 1: Prerequisites and setup](/operate/modules/support-hardware/part-1-prerequisites-setup/)
+- **Current:** Part 2: Choose an API and generate code
+- **Next:** [Part 3: Implement your module →](/operate/modules/support-hardware/part-3-implement-single-model/)
+- **All parts:** [Module creation tutorial](/operate/modules/support-hardware/)
diff --git a/docs/operate/modules/support-hardware/part-3-implement-single-model.md b/docs/operate/modules/support-hardware/part-3-implement-single-model.md
new file mode 100644
index 0000000000..f3651f2837
--- /dev/null
+++ b/docs/operate/modules/support-hardware/part-3-implement-single-model.md
@@ -0,0 +1,375 @@
+---
+title: "Part 3: Implement your module"
+linkTitle: "Part 3: Implement"
+weight: 33
+layout: "docs"
+type: "docs"
+description: "Implement configuration validation, reconfiguration, and API methods for your module."
+---
+
+**Part 3 of 5** | ⏱️ 30-40 minutes
+
+## What you'll do in this part
+
+- Implement configuration validation for your model
+- Set up reconfiguration to use config values
+- Implement the camera's `GetImages` method
+- Understand how to implement API methods in general
+
+At the end of this part, you'll have a working single-model module that returns images.
+
+## Implementation overview
+
+You'll implement your module in three main steps:
+
+1. **Validation**: Check that user configuration is correct
+2. **Reconfiguration**: Use configuration values in your code
+3. **API methods**: Implement the actual functionality
+
+## Step 1: Configuration validation
+
+### Why validation matters
+
+When users add your module to their machine, they provide configuration in JSON format. Validation ensures:
+- Required attributes are present
+- Attributes are the correct type
+- You can provide helpful error messages early
+
+For the example camera model, users will configure it like this:
+
+```json
+{
+ "image_path": "/path/to/file.png"
+}
+```
+
+Your validation must verify that `image_path` exists and is a string.
+
+### Implement validate_config
+
+Model configuration happens in two steps:
+
+#### Validation
+
+The validation step serves two purposes:
+
+- Confirm that the model configuration contains all **required attributes** and that these attributes are of the right type.
+- Identify and return a list of names of **required resources** and a list of names of **optional resources**.
+ `viam-server` will pass these resources to the next step as dependencies.
+ For more information, see [Module dependencies](/operate/modules/advanced/dependencies/).
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+Open `src/models/hello_camera.py` and find the `validate_config` method (around line 38).
+
+Replace it with:
+
+```python {class="line-numbers linkable-line-numbers" data-start="38" data-line="5-10"}
+ @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:**
+- **Lines 43-44**: Get the configuration attributes as a dictionary
+- **Lines 45-46**: Check if `image_path` exists in the config
+- **Lines 47-48**: Verify it's a string type (not a number or other type)
+- **Line 50**: Return `(required_dependencies, optional_dependencies)` - we have none for this simple camera
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+Open `hello-world/module.go` and find the `Validate` function (around line 51).
+
+Replace it with:
+
+```go {class="line-numbers linkable-line-numbers" data-start="51" data-line="2-10"}
+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:
+
+```go {class="line-numbers linkable-line-numbers" data-start="7"}
+"reflect"
+```
+
+**Code explanation:**
+- **Line 53**: Check if `image_path` is empty (required field)
+- **Lines 54-55**: Return error with helpful message if missing
+- **Lines 56-58**: Verify it's a string type
+- **Line 59**: Store the path in a global variable for later use
+- **Line 60**: Return `(required_deps, optional_deps, error)` - we have no dependencies
+
+{{% /tab %}}
+{{< /tabs >}}
+
+✅ **Checkpoint 1:** Configuration validation implemented
+
+{{< expand "What happens if validation fails?" >}}
+If a user misconfigures your module (forgets `image_path` or uses the wrong type), `viam-server` will:
+1. Refuse to start the module
+2. Show your error message in the configuration panel
+3. Display the error in logs
+
+This prevents runtime errors and helps users debug configuration issues quickly.
+{{< /expand >}}
+
+## Step 2: Reconfiguration
+
+### Why reconfiguration matters
+
+After validation succeeds, `viam-server` calls the `reconfigure` method. This is where you:
+- Store configuration values for use in API methods
+- Set up any stateful resources
+- Access dependencies from other components
+
+#### Reconfiguration
+
+`viam-server` calls the `reconfigure` method when the user adds the model or changes its configuration.
+
+The reconfiguration step serves two purposes:
+
+- Use the configuration attributes and dependencies to set attributes on the model for usage within the API methods.
+- Obtain access to dependencies.
+ For information on how to use dependencies, see [Module dependencies](/operate/modules/advanced/dependencies/).
+
+### Implement reconfigure
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+Open `src/models/hello_camera.py` and find the `reconfigure` method (around line 51).
+
+Replace it with:
+
+```python {class="line-numbers linkable-line-numbers" data-start="51" data-line="4-5"}
+ 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:
+
+```python {class="line-numbers linkable-line-numbers" data-start="1"}
+from viam.utils import struct_to_dict
+```
+
+**Code explanation:**
+- **Line 54**: Convert config attributes to a Python dictionary
+- **Line 55**: Store `image_path` as an instance variable for use in API methods
+- **Line 57**: Call parent class reconfigure (important!)
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+For Go modules, configuration handling is done differently:
+
+1. Open `hello-world/module.go`
+
+2. Add `imagePath = ""` to the global variables (around line 18):
+
+ ```go {class="line-numbers linkable-line-numbers" data-start="18" data-line="4"}
+ var (
+ HelloCamera = resource.NewModel("exampleorg", "hello-world", "hello-camera")
+ errUnimplemented = errors.New("unimplemented")
+ imagePath = ""
+ )
+ ```
+
+3. Edit the `type Config struct` definition (around line 32), replacing the comments with:
+
+ ```go {class="line-numbers linkable-line-numbers" data-start="32"}
+ 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.
+
+{{< expand "Need to maintain state across reconfigurations?" >}}
+The `resource.AlwaysRebuild` parameter causes `viam-server` to fully rebuild the resource each time configuration changes.
+
+If you need to maintain state (like keeping PWM loops running for a board), implement a `Reconfigure` function instead:
+
+```go
+func (c *helloWorldHelloCamera) Reconfigure(ctx context.Context, deps resource.Dependencies, conf resource.Config) error {
+ // Update configuration values
+ imagePath = cfg.ImagePath
+ // Keep existing state alive
+ return nil
+}
+```
+
+For an example, see [mybase.go on GitHub](https://github.com/viamrobotics/rdk/blob/main/examples/customresources/models/mybase/mybase.go).
+{{< /expand >}}
+
+{{% /tab %}}
+{{< /tabs >}}
+
+✅ **Checkpoint 2:** Reconfiguration implemented
+
+## Step 3: Implement API methods
+
+Now comes the core functionality: implementing the methods from your chosen API.
+
+### Camera API: Implement GetImages
+
+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.
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+Open `src/models/hello_camera.py` and find the `get_images` method (around line 74).
+
+Replace `raise NotImplementedError()` with:
+
+```python {class="line-numbers linkable-line-numbers" data-start="74" data-line="9-13"}
+ 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:
+
+```python {class="line-numbers linkable-line-numbers" data-start="1"}
+from viam.media.utils.pil import pil_to_viam_image
+from viam.media.video import CameraMimeType
+from PIL import Image
+```
+
+**Code explanation:**
+- **Line 82**: Open the image file using the path from configuration
+- **Line 83**: Convert PIL image to Viam's image format
+- **Line 84**: Create a NamedImage with source name "default"
+- **Line 85**: Create response metadata (can include timing info)
+- **Line 86**: Return list of images and metadata
+
+**Add the Pillow dependency:**
+
+Open `requirements.txt` and add:
+
+```text
+Pillow
+```
+
+Save the file.
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+Open `hello-world/module.go` and find the `Images` method (around line 111).
+
+Replace `panic("not implemented")` with:
+
+```go {class="line-numbers linkable-line-numbers" data-start="111"}
+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:
+
+```go {class="line-numbers linkable-line-numbers" data-start="7"}
+"os"
+```
+
+**Code explanation:**
+- **Lines 114-117**: Open the image file
+- **Lines 119-122**: Read file contents into byte array
+- **Lines 124-127**: Create a NamedImage from bytes
+- **Line 129**: Return slice with one image, metadata, and no error
+
+{{% /tab %}}
+{{< /tabs >}}
+
+### What about other camera methods?
+
+The camera API includes methods like `GetPointCloud`, `GetProperties`, and `DoCommand`. You don't need to implement all of them:
+
+- **Unimplemented methods** return an "unimplemented" error automatically
+- **DoCommand** can be used for custom functionality (see [Run control logic](/operate/modules/control-logic/))
+
+For this camera, we only implement `GetImages` because that's all our hardware supports.
+
+✅ **Checkpoint 3:** Camera implementation complete
+
+## What you've accomplished
+
+✅ **Module implemented:**
+- Configuration validation ensures correct setup
+- Reconfiguration stores config values for use
+- GetImages method returns images from the file path
+
+✅ **Understanding:**
+- How modules validate and use configuration
+- How to implement API methods
+- What happens with unimplemented methods
+
+✅ **Ready to test:**
+- Have a complete, working module
+- Ready to test on a real machine
+
+## Next steps
+
+Your module is now ready to test! Continue to [Part 4: Test your module locally](/operate/modules/support-hardware/part-4-test-locally/) to see your module in action.
+
+---
+
+**Tutorial navigation:**
+- **Previous:** [← Part 2: Choose an API and generate code](/operate/modules/support-hardware/part-2-choose-api-generate/)
+- **Current:** Part 3: Implement your module
+- **Next:** [Part 4: Test your module locally →](/operate/modules/support-hardware/part-4-test-locally/)
+- **All parts:** [Module creation tutorial](/operate/modules/support-hardware/)
diff --git a/docs/operate/modules/support-hardware/part-4-test-locally.md b/docs/operate/modules/support-hardware/part-4-test-locally.md
new file mode 100644
index 0000000000..b33bc13857
--- /dev/null
+++ b/docs/operate/modules/support-hardware/part-4-test-locally.md
@@ -0,0 +1,297 @@
+---
+title: "Part 4: Test your module locally"
+linkTitle: "Part 4: Test locally"
+weight: 34
+layout: "docs"
+type: "docs"
+description: "Test your module on a real machine before deploying to the registry."
+---
+
+**Part 4 of 5** | ⏱️ 20 minutes
+
+## What you'll do in this part
+
+- Add your module to a test machine using hot reload
+- Configure your camera component
+- Test the camera in the Viam app
+- Learn the iteration workflow
+- Troubleshoot common issues
+
+You can test your module locally before uploading it to the [registry](https://app.viam.com/registry).
+
+## Test with hot reload (recommended)
+
+Hot reload is the fastest way to test your module. It automatically builds your module and deploys it to your machine for testing.
+
+{{< alert title="Why hot reload?" color="tip" >}}
+Hot reload handles cross-compilation automatically, so you can develop on your laptop (macOS/Windows) and test on a Raspberry Pi (Linux ARM) without any extra tools.
+{{< /alert >}}
+
+### Add module to your machine
+
+Run the following command from your module directory to build and deploy your module:
+
+{{< tabs >}}
+{{% tab name="Same device" %}}
+
+If you're developing on the same device where `viam-server` is running:
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+viam module reload-local --cloud-config /path/to/viam.json
+```
+
+Replace `/path/to/viam.json` with the path to your machine's cloud config file (usually in `/etc/viam/` on Linux or downloaded from the Viam app).
+
+{{% /tab %}}
+{{% tab name="Other device" %}}
+
+If you're developing on a different device from where `viam-server` is running:
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+viam module reload --part-id 123abc45-1234-432c-aabc-z1y111x23a00
+```
+
+Replace the part ID with your machine's part ID. Find it by:
+1. Go to your machine's page in the Viam app
+2. Click the **Live** indicator
+3. Click **Part ID** to copy it
+
+{{% /tab %}}
+{{< /tabs >}}
+
+For more information, see the [`viam module` CLI documentation](/dev/tools/cli/#module).
+
+The command will:
+1. Build your module using your local architecture or the target machine's architecture
+2. Package it with all dependencies
+3. Use the shell service to copy it to your machine
+4. Restart the module automatically
+
+{{< alert title="Success!" color="note" >}}
+You may need to refresh your machine page in the Viam app for your module to appear.
+{{< /alert >}}
+
+{{< expand "Troubleshooting hot reload" >}}
+
+**Error: Could not connect to machine part: context deadline exceeded**
+
+Try specifying the `--part-id` explicitly (find it on your machine's page by clicking **Live** → **Part ID**).
+
+**Error: Rpc error: code = Unknown desc = stat /root/.viam/packages-local: no such file or directory**
+
+Try specifying the `--home` directory:
+```sh
+viam module reload --part-id YOUR_PART_ID --home /Users/yourname/
+```
+
+**Error: Error while refreshing token, logging out. Please log in again**
+
+Run `viam login` to reauthenticate the CLI.
+
+**Still having problems?**
+
+You can use manual testing (see below) as an alternative.
+
+{{< /expand >}}
+
+{{< expand "Alternative: Manual testing" >}}
+
+If hot reload isn't working, you can manually add your module:
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+1. Navigate to your machine's **CONFIGURE** page
+2. Click the **+** button, select **Local module**, then select **Local module** again
+3. Enter the path to the `run.sh` script, for example: `/home/yourname/hello-world/run.sh`
+4. Click **Create**
+5. Save the config
+
+For local modules, `viam-server` uses this path to start the module.
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+1. From your module directory, compile your module:
+ ```sh {class="command-line" data-prompt="$"}
+ viam module build local
+ ```
+
+2. Navigate to your machine's **CONFIGURE** page
+3. Click the **+** button, select **Local module**, then select **Local module** again
+4. Enter the path to the executable, for example: `/home/yourname/hello-world/bin/hello-world`
+5. Click **Create**
+6. Save the config
+
+For local modules, `viam-server` uses this path to start the module.
+
+{{% /tab %}}
+{{< /tabs >}}
+
+{{< /expand >}}
+
+## Configure your component
+
+Now that your module is added to your machine, configure a component that uses it:
+
+1. On your machine's **CONFIGURE** page, click **+**
+2. Select **Local module**, then **Local component**
+3. Enter the {{< glossary_tooltip term_id="model-namespace-triplet" text="model namespace triplet" >}}, for example: `exampleorg:hello-world:hello-camera`
+ - You can find this in the `model` field of your `meta.json` file
+4. Select **Camera** as the Type
+5. Enter a Name, such as `camera-1`
+6. Click **Create**
+
+### Add configuration attributes
+
+In the configuration panel that appears, add your model's required attributes.
+
+For the camera model, replace `{}` with:
+
+```json {class="line-numbers linkable-line-numbers"}
+{
+ "image_path": "/path/to/your/image.png"
+}
+```
+
+Replace `/path/to/your/image.png` with an actual path to an image file on your machine.
+
+{{< alert title="Tip" color="tip" >}}
+Make sure the image file exists and `viam-server` has permission to read it!
+{{< /alert >}}
+
+Save the config and wait a few seconds for it to apply.
+
+## Test your component
+
+Click the **TEST** section at the bottom of your camera's configuration card.
+
+If everything is working correctly, you should see your image displayed in the test panel!
+
+{{}}
+
+### If you see errors
+
+Errors will appear in:
+- The configuration panel (red banner)
+- The **LOGS** tab (click **LOGS** in the top navigation)
+
+Common issues:
+- **"Missing image_path attribute"**: Add `image_path` to your config JSON
+- **"Error opening image"**: Check that the file path is correct and readable
+- **Module not found**: Refresh the page or restart the module
+
+## Iterate on your module
+
+Each time you make changes to your module code, update it on your machine:
+
+{{< tabs >}}
+{{% tab name="Hot reload (recommended)" %}}
+
+Run the reload command again:
+
+{{< tabs >}}
+{{% tab name="Same device" %}}
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+viam module reload-local --cloud-config /path/to/viam.json
+```
+
+{{% /tab %}}
+{{% tab name="Other device" %}}
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+viam module reload --part-id 123abc45-1234-432c-aabc-z1y111x23a00
+```
+
+{{% /tab %}}
+{{< /tabs >}}
+
+Your machine may already have a previously published version of the module you're iterating on. If so, you can toggle **Hot Reloading** on and off in the module's configuration card in the Viam app:
+- **On**: Uses your local development version
+- **Off**: Uses the published registry version
+
+{{% /tab %}}
+{{% tab name="Manual testing" %}}
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+Save your code changes, then restart the module in your machine's **CONFIGURE** tab:
+
+1. Find the module's card
+2. Click the **...** menu in the upper-right corner
+3. Click **Restart**
+
+{{}}
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+1. Rebuild your module:
+ ```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+ viam module build local
+ ```
+
+2. Restart it in your machine's **CONFIGURE** tab:
+ - Find the module's card
+ - Click the **...** menu
+ - Click **Restart**
+
+{{}}
+
+{{% /tab %}}
+{{< /tabs >}}
+
+{{% /tab %}}
+{{< /tabs >}}
+
+### Development workflow
+
+The typical development cycle:
+
+1. **Edit code** in your module files
+2. **Reload/restart** the module on your machine
+3. **Test** using the TEST panel or Viam app
+4. **Check logs** if something doesn't work
+5. **Repeat** until it works as expected
+
+{{< alert title="Debugging tip" color="tip" >}}
+Use logging in your module code to help debug. Logs appear in the **LOGS** tab in the Viam app.
+
+Python: `logger.info("Message")`
+Go: `logger.Info("Message")`
+{{< /alert >}}
+
+## What you've accomplished
+
+✅ **Module deployed:**
+- Used hot reload to build and deploy your module
+- Module running on a real machine
+
+✅ **Component configured:**
+- Added camera component with proper configuration
+- Verified configuration validation works
+
+✅ **Testing complete:**
+- Tested camera in Viam app
+- Confirmed images are returned correctly
+
+✅ **Ready for more:**
+- Understand the iteration workflow
+- Know how to debug issues
+
+## Next steps
+
+Your module works! Now you have two options:
+
+1. **Ready to deploy?** Once you've thoroughly tested your module, continue to [Package and deploy your module](/operate/modules/deploy-module/) to upload it to the registry
+2. **Want to add more models?** Continue to [Part 5: Multiple models](/operate/modules/support-hardware/part-5-multiple-models/) to learn how to add the sensor model to your module
+
+---
+
+**Tutorial navigation:**
+- **Previous:** [← Part 3: Implement your module](/operate/modules/support-hardware/part-3-implement-single-model/)
+- **Current:** Part 4: Test your module locally
+- **Next:** [Part 5: Multiple models (advanced) →](/operate/modules/support-hardware/part-5-multiple-models/) or [Deploy to registry →](/operate/modules/deploy-module/)
+- **All parts:** [Module creation tutorial](/operate/modules/support-hardware/)
diff --git a/docs/operate/modules/support-hardware/part-5-multiple-models.md b/docs/operate/modules/support-hardware/part-5-multiple-models.md
new file mode 100644
index 0000000000..f30346f5dd
--- /dev/null
+++ b/docs/operate/modules/support-hardware/part-5-multiple-models.md
@@ -0,0 +1,433 @@
+---
+title: "Part 5: Multiple models (advanced)"
+linkTitle: "Part 5: Multiple models"
+weight: 35
+layout: "docs"
+type: "docs"
+description: "Learn how to create a module with multiple models implementing different APIs."
+---
+
+**Part 5 of 5** | ⏱️ 20-25 minutes | **Advanced**
+
+{{< alert title="Prerequisites" color="note" >}}
+Complete Parts 1-4 first. You should have a working single-model module before adding more models.
+{{< /alert >}}
+
+## What you'll do in this part
+
+- Understand when to use multiple models in one module
+- Generate code for a second model (sensor)
+- Integrate both models into one module
+- Implement the sensor's `GetReadings` method
+- Test both models together
+
+## When to use multiple models
+
+Some of the code you generated for your first modular resource is shared across the module no matter how many modular resource models it supports. Some of the code is resource-specific.
+
+If you have multiple modular resources that are related, you can put them all into the same module.
+
+**Use multiple models when:**
+- The hardware provides multiple capabilities (like our camera + sensor example)
+- Models share dependencies or configuration
+- They're logically related and users would configure them together
+
+**Use separate modules when:**
+- The functionality is unrelated
+- Each model would be useful independently
+- You want to version them separately
+
+### Example: Why two models?
+
+Our example hardware:
+- Returns an image (camera functionality)
+- Returns a random number (sensor functionality)
+
+Since the [Camera API](/dev/reference/apis/components/camera/) can't return numbers and the [Sensor API](/dev/reference/apis/components/sensor/) can't return images, we need two models.
+
+## Generate the second model
+
+For convenience, run the module generator again from within your existing module's directory to generate code for the sensor:
+
+1. Change directory into your module:
+ ```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+ cd hello-world
+ ```
+
+2. Run the generator for the sensor model:
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+viam module generate --language python --model-name hello-sensor \
+ --name hello-world --resource-subtype=sensor --public false \
+ --enable-cloud true
+```
+
+{{< alert title="Important" color="caution" >}}
+When prompted whether to register the module, select **No**. Your module is already registered.
+{{< /alert >}}
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+viam module generate --language go --model-name hello-sensor \
+ --name hello-world --resource-subtype=sensor --public false \
+ --enable-cloud true
+```
+
+{{< alert title="Important" color="caution" >}}
+When prompted whether to register the module, select **No**. Your module is already registered.
+{{< /alert >}}
+
+{{% /tab %}}
+{{< /tabs >}}
+
+This creates a temporary nested `hello-world/hello-world/` directory. You'll copy the sensor-specific code from it.
+
+## Integrate sensor code
+
+Now integrate the sensor model into your existing module:
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+### 1. Move the sensor model file
+
+Move the generated sensor model file:
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+mv hello-world/src/models/hello_sensor.py src/models/
+```
+
+### 2. Update main.py
+
+Open `src/main.py` and add `HelloSensor` to the imports:
+
+```python {class="line-numbers linkable-line-numbers" data-line="6, 9"}
+import asyncio
+
+from viam.module.module import Module
+try:
+ from models.hello_camera import HelloCamera
+ from models.hello_sensor import HelloSensor
+except ModuleNotFoundError: # when running as local module with run.sh
+ from .models.hello_camera import HelloCamera
+ from .models.hello_sensor import HelloSensor
+
+if __name__ == '__main__':
+ asyncio.run(Module.run_from_registry())
+```
+
+Save the file.
+
+### 3. Move model documentation
+
+Move the sensor documentation file:
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+mv hello-world/_hello-world_hello-sensor.md ./
+```
+
+### 4. Update meta.json
+
+Open `meta.json` and update the description to mention both models:
+
+```json {class="line-numbers linkable-line-numbers" data-line="6"}
+{
+ "$schema": "https://dl.viam.dev/module.schema.json",
+ "module_id": "exampleorg:hello-world",
+ "visibility": "private",
+ "url": "",
+ "description": "Example camera and sensor components: hello-camera and hello-sensor",
+ "applications": null,
+ "markdown_link": "README.md",
+ "entrypoint": "./run.sh",
+ "first_run": "",
+ "build": {
+ "build": "./build.sh",
+ "setup": "./setup.sh",
+ "path": "dist/archive.tar.gz",
+ "arch": ["linux/amd64", "linux/arm64", "darwin/arm64", "windows/amd64"]
+ }
+}
+```
+
+Save the file.
+
+### 5. Clean up
+
+Delete the temporary directory:
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+rm -rf hello-world/
+```
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+### 1. Rename the camera model file
+
+Rename `module.go` to `hello-camera.go`:
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+mv module.go hello-camera.go
+```
+
+### 2. Move the sensor model file
+
+Move and rename the sensor model file:
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+mv hello-world/module.go hello-sensor.go
+```
+
+### 3. Update cmd/module/main.go
+
+Open `cmd/module/main.go` and register both models:
+
+```go {class="line-numbers linkable-line-numbers" data-start="1" data-line="8, 13-16"}
+package main
+
+import (
+ "helloworld"
+ "go.viam.com/rdk/module"
+ "go.viam.com/rdk/resource"
+ camera "go.viam.com/rdk/components/camera"
+ sensor "go.viam.com/rdk/components/sensor"
+)
+
+func main() {
+ // ModularMain can take multiple APIModel arguments, if your module implements multiple models.
+ module.ModularMain(
+ resource.APIModel{ camera.API, helloworld.HelloCamera},
+ resource.APIModel{ sensor.API, helloworld.HelloSensor},
+ )
+}
+```
+
+Save the file.
+
+### 4. Move model documentation
+
+Move the sensor documentation file:
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+mv hello-world/_hello-world_hello-sensor.md ./
+```
+
+### 5. Update meta.json
+
+Open `meta.json` and update the description to mention both models:
+
+```json {class="line-numbers linkable-line-numbers" data-line="6"}
+{
+ "$schema": "https://dl.viam.dev/module.schema.json",
+ "module_id": "exampleorg:hello-world",
+ "visibility": "private",
+ "url": "",
+ "description": "Example camera and sensor components: hello-camera and hello-sensor",
+ "applications": null,
+ "markdown_link": "README.md",
+ "entrypoint": "bin/hello-world",
+ "first_run": "",
+ "build": {
+ "build": "make module.tar.gz",
+ "setup": "make setup",
+ "path": "module.tar.gz",
+ "arch": ["linux/amd64", "linux/arm64", "darwin/arm64", "windows/amd64"]
+ }
+}
+```
+
+Save the file.
+
+### 6. Fix duplicate definitions in hello-sensor.go
+
+Since `errUnimplemented` and `Config` are already defined in `hello-camera.go`, update `hello-sensor.go`:
+
+1. **Delete the `"errors"` import** (if it exists only for `errUnimplemented`)
+2. **Delete the line:** `errUnimplemented = errors.New("unimplemented")`
+3. **Rename** `type Config struct {` to `type sensorConfig struct {`
+4. **Replace all instances** of `*Config` in `hello-sensor.go` with `*sensorConfig`
+
+### 7. Clean up
+
+Delete the temporary directory:
+
+```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+rm -rf hello-world/
+```
+
+{{% /tab %}}
+{{< /tabs >}}
+
+✅ **Checkpoint 1:** Sensor code integrated
+
+## Implement the sensor API
+
+Now implement the `GetReadings` method for the sensor:
+
+{{< tabs >}}
+{{% tab name="Python" %}}
+
+Open `src/models/hello_sensor.py` and find the `get_readings` method (around line 63).
+
+Replace `raise NotImplementedError()` with:
+
+```python {class="line-numbers linkable-line-numbers" data-start="63" data-line="8-11"}
+ async def get_readings(
+ self,
+ *,
+ extra: Optional[Mapping[str, Any]] = None,
+ timeout: Optional[float] = None,
+ **kwargs
+ ) -> Mapping[str, SensorReading]:
+ number = random.random()
+ return {
+ "random_number": number
+ }
+```
+
+Add the import at the top of the file:
+
+```python {class="line-numbers linkable-line-numbers" data-start="1"}
+import random
+```
+
+Save the file.
+
+**Code explanation:**
+- **Line 70**: Generate a random number between 0 and 1
+- **Lines 71-73**: Return a dictionary with the reading name and value
+- Sensor readings are returned as key-value pairs (you can return multiple readings)
+
+{{% /tab %}}
+{{% tab name="Go" %}}
+
+Open `hello-sensor.go` and find the `Readings` method (around line 92).
+
+Replace `panic("not implemented")` with:
+
+```go {class="line-numbers linkable-line-numbers" data-start="92"}
+func (s *helloWorldHelloSensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) {
+ number := rand.Float64()
+ return map[string]interface{}{
+ "random_number": number,
+ }, nil
+}
+```
+
+Add the import at the top of the file:
+
+```go {class="line-numbers linkable-line-numbers" data-start="7"}
+"math/rand"
+```
+
+Save the file.
+
+**Code explanation:**
+- **Line 93**: Generate a random number between 0 and 1
+- **Lines 94-96**: Return a map with the reading name and value
+- **Line 97**: Return no error (nil)
+
+{{% /tab %}}
+{{< /tabs >}}
+
+Note that the sensor has no configurable attributes, so you don't need to modify the validation or reconfiguration methods.
+
+✅ **Checkpoint 2:** Sensor implementation complete
+
+## Test both models
+
+Now test your multi-model module:
+
+1. **Reload the module** using hot reload:
+ ```sh {id="terminal-prompt" class="command-line" data-prompt="$"}
+ viam module reload --part-id YOUR_PART_ID
+ ```
+
+2. **Add the sensor component:**
+ - On your machine's **CONFIGURE** page, click **+**
+ - Select **Local module** → **Local component**
+ - Enter the model: `exampleorg:hello-world:hello-sensor`
+ - Select **Sensor** as the Type
+ - Enter a Name: `sensor-1`
+ - Click **Create**
+
+3. **Test the sensor:**
+ - Click the **TEST** section of the sensor card
+ - You should see `{"random_number": 0.xxxxx}` with a random number
+
+4. **Verify camera still works:**
+ - Test your camera component again
+ - Both should work independently
+
+{{}}
+
+✅ **Checkpoint 3:** Both models working!
+
+## Best practices
+
+### When adding multiple models:
+
+1. **Keep models independent**: Each model should work without the others
+2. **Share common code**: Put shared utilities in separate files
+3. **Document each model**: Each model should have its own documentation file
+4. **Test individually**: Verify each model works before combining
+5. **Version together**: All models in a module share the same version number
+
+### Module organization
+
+For modules with many models, consider this structure:
+
+```
+my-module/
+├── src/
+│ ├── models/
+│ │ ├── model1.py
+│ │ ├── model2.py
+│ │ └── model3.py
+│ ├── utils/
+│ │ └── shared.py
+│ └── main.py
+```
+
+## What you've accomplished
+
+✅ **Multi-model module created:**
+- Generated second model (sensor)
+- Integrated both models into one module
+- Both models register and work independently
+
+✅ **Complete implementation:**
+- Camera returns images from configured path
+- Sensor returns random number readings
+- Both tested and working
+
+✅ **Understanding:**
+- When to use multiple models vs. separate modules
+- How to integrate multiple models
+- Best practices for module organization
+
+## Next steps
+
+Congratulations! You've created a complete, working module with multiple models. Now you're ready to:
+
+1. **Deploy to the registry**: [Package and deploy your module](/operate/modules/deploy-module/)
+2. **Share with others**: Make your module public so others can use it
+3. **Add more features**: Implement additional models or enhance existing ones
+4. **Learn more advanced topics**:
+ - [Module dependencies](/operate/modules/advanced/dependencies/)
+ - [Custom configuration options](/operate/modules/advanced/module-configuration/)
+ - [Logging in modules](/operate/modules/advanced/logging/)
+
+---
+
+**Tutorial navigation:**
+- **Previous:** [← Part 4: Test your module locally](/operate/modules/support-hardware/part-4-test-locally/)
+- **Current:** Part 5: Multiple models (advanced)
+- **Next:** [Deploy to registry →](/operate/modules/deploy-module/)
+- **All parts:** [Module creation tutorial](/operate/modules/support-hardware/)
diff --git a/package-lock.json b/package-lock.json
index 374e6905a6..f431993686 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -324,9 +324,10 @@
}
},
"node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -932,9 +933,9 @@
}
},
"node_modules/diff": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
- "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
+ "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -1766,12 +1767,13 @@
}
},
"node_modules/micromatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
- "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "braces": "^3.0.2",
+ "braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@@ -1854,9 +1856,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
@@ -1864,6 +1866,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -3369,9 +3372,9 @@
}
},
"ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"requires": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -3776,9 +3779,9 @@
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="
},
"diff": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
- "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
+ "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
"dev": true
},
"dir-glob": {
@@ -4359,12 +4362,12 @@
"dev": true
},
"micromatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
- "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"requires": {
- "braces": "^3.0.2",
+ "braces": "^3.0.3",
"picomatch": "^2.3.1"
}
},
@@ -4421,9 +4424,9 @@
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ=="
},
"nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true
},
"node-addon-api": {