# Write a driver module

Build a module that implements a resource API and runs as a separate process.
> Source: https://docs.viam.com/build-modules/write-a-driver-module/


You want to use hardware that Viam doesn't support out of the box, whether it's a sensor, camera, motor, or any other component. A driver module bridges that gap: it implements a standard Viam resource API so that data capture, the Test section, the SDKs, and other platform features work with your hardware automatically.

Driver modules run as separate processes alongside `viam-server`, so they carry their own dependencies and can crash without bringing `viam-server` down. You package and distribute them through the Viam registry.

This page walks through seven steps for writing a driver module, using a temperature-and-humidity sensor as the worked example. For background on choosing a resource API, module lifecycle, and dependencies, see the [overview](/build-modules/overview/).

## Steps

When writing a module, follow the steps outlined below. To illustrate each step we'll use a sensor module as a worked example. The same patterns
apply to any resource type -- substitute the appropriate API and methods for
your use case.

### 1. Generate the module

Before you run the generator, [install the Viam CLI](/cli/overview/#install) and log in with `viam login`.

The generator prompts for your organization's public namespace. If you have not set one yet, click the organization dropdown at the upper right of the Viam app, select **Settings**, then **Set a public namespace**. You can also enter your Org ID at the prompt instead.

Run the Viam CLI generator:

```bash
viam module generate
```

The generator creates a new directory named after your module (for example, `my-sensor-module`) in your current working directory. `cd` into that directory for the rest of the steps.

When run without flags, the generator prompts for each value below. If you pass these as `--name`, `--language`, `--visibility`, `--public-namespace`, `--resource-subtype`, `--model-name`, and `--register` flags instead, use the flag forms noted in the table (where different from the interactive labels).

| Prompt                                       | What to enter               | Why                                                                                                                                                                                                      |
| -------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Set a module name:                           | `my-sensor-module`          | A short, descriptive name                                                                                                                                                                                |
| Specify the language for the module:         | `python` or `go`            | Your implementation language                                                                                                                                                                             |
| Visibility:                                  | `private`                   | `private`: visible only within your org. `public`: visible to everyone. `public_unlisted`: usable by anyone who knows the module ID, but hidden from the registry page. You can change visibility later. |
| Namespace/Organization ID                    | Your organization namespace | Scopes the module to your org                                                                                                                                                                            |
| Select a resource to be added to the module: | `Sensor Component`          | The resource API to implement                                                                                                                                                                            |
| Set a model name of the resource:            | `my-sensor`                 | The model name for your sensor                                                                                                                                                                           |
| Register module                              | `yes`                       | Registers the module with Viam                                                                                                                                                                           |

The generator creates a complete project with the following files:




### Python

<table>
  <thead>
      <tr>
          <th>File</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>`src/main.py`</td>
          <td>Entry point – starts the module server</td>
      </tr>
      <tr>
          <td>`src/models/my_sensor.py`</td>
          <td>Resource class skeleton – you will edit this</td>
      </tr>
      <tr>
          <td>`requirements.txt`</td>
          <td>Python dependencies</td>
      </tr>
      <tr>
          <td>`meta.json`</td>
          <td>Module metadata for the registry</td>
      </tr>
      <tr>
          <td>`setup.sh`</td>
          <td>Installs dependencies into a virtualenv</td>
      </tr>
      <tr>
          <td>`build.sh`</td>
          <td>Packages the module for upload</td>
      </tr>
      <tr>
          <td>`.github/workflows/deploy.yml`</td>
          <td>CI workflow for cloud builds</td>
      </tr>
  </tbody>
</table>

### Go

<table>
  <thead>
      <tr>
          <th>File</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>`cmd/module/main.go`</td>
          <td>Entry point – starts the module server</td>
      </tr>
      <tr>
          <td>`module.go`</td>
          <td>Resource implementation skeleton – you will edit this</td>
      </tr>
      <tr>
          <td>`go.mod`</td>
          <td>Go module definition</td>
      </tr>
      <tr>
          <td>`Makefile`</td>
          <td>Build targets</td>
      </tr>
      <tr>
          <td>`meta.json`</td>
          <td>Module metadata for the registry</td>
      </tr>
      <tr>
          <td>`.github/workflows/deploy.yml`</td>
          <td>CI workflow for cloud builds</td>
      </tr>
  </tbody>
</table>



### 2. Implement the resource API

Open the generated resource file: `src/models/my_sensor.py` (Python) or `module.go` (Go). The generator creates a class (Python) or struct (Go) with stub methods. You need to make four changes to the resource file, then review the entry point the generator created:

1. Define your config attributes.
2. Add validation logic.
3. Populate your resource from config in the constructor.
4. Implement the API methods for your resource type.

The following example builds a sensor that reads temperature and humidity from
a custom HTTP API endpoint. Replace the HTTP call with whatever data source
your sensor uses.

#### Define your config attributes

Config attributes are the fields a user sets when they configure your component
in the Viam app. The generator creates an empty config; add a field for each
attribute your module needs.




### Python

In `src/models/my_sensor.py`, declare your config attributes as type-annotated
class variables. They will be populated from the config in `new`:

```python
class MySensor(Sensor, EasyResource):
    # To enable debug-level logging, either run viam-server with the --debug option,
    # or configure your resource/machine to display debug logs.
    MODEL: ClassVar[Model] = Model(
        ModelFamily("my-org", "my-sensor-module"), "my-sensor"
    )

    # Add your config attributes as instance variables
    source_url: str
    poll_interval: float
```

### Go

In `module.go`, find the empty `Config` struct and add fields.
Each field needs a `json` tag that matches the attribute name users will set in
their config JSON:

```go
type Config struct {
    SourceURL    string  `json:"source_url"`
    PollInterval float64 `json:"poll_interval"`
}
```



#### Add validation logic

The generator creates an empty validation method. Add checks for required
fields and return any [dependencies](/build-modules/dependencies/) your module needs.




### Python

Find `validate_config` in your class and add validation:

```python
@classmethod
    def validate_config(
        cls, config: ComponentConfig
    ) -> Tuple[Sequence[str], Sequence[str]]:
        fields = config.attributes.fields
        if "source_url" not in fields:
            raise Exception("source_url is required")
        if not fields["source_url"].string_value.startswith("http"):
            raise Exception("source_url must be an HTTP or HTTPS URL")
        return [], []  # No required or optional dependencies
```

### Go

Find the `Validate` method on your `Config` struct and add validation:

```go
func (cfg *Config) Validate(path string) ([]string, []string, error) {
    if cfg.SourceURL == "" {
        return nil, nil, fmt.Errorf("source_url is required")
    }
    return nil, nil, nil // No required or optional dependencies
}
```



The validation method returns two lists: required dependencies and optional
dependencies. In Go, the method also returns an `error` as a third value; in
Python, raise an exception instead. For a standalone sensor with no
dependencies, return empty lists and no error.
See [Step 5](#5-handle-dependencies) for modules that depend on other
components.

#### Populate your resource from config

Your constructor runs on both initial creation and every config change, so read config fields and initialize state there. When a user changes the configuration, `viam-server` stops the existing resource instance and creates a fresh one with the new config.




### Python

Override `new` to read your config and set the fields on the instance before
returning it:

```python
@classmethod
    def new(cls, config: ComponentConfig,
            dependencies: Mapping[ResourceName, ResourceBase]) -> Self:
        sensor = super().new(config, dependencies)
        fields = config.attributes.fields
        sensor.source_url = fields["source_url"].string_value
        sensor.poll_interval = (
            fields["poll_interval"].number_value
            if "poll_interval" in fields
            else 10.0
        )
        return sensor
```
The generator also emits `do_command`, `get_status`, and `get_geometries`
stubs that raise `NotImplementedError`. Leave them as-is unless your module
needs custom handling.

### Go

The generator produces two constructor functions: a private
`newMySensorModuleMySensor` that unpacks the raw config and delegates to a
public `NewMySensor` that takes a typed `*Config`. The split lets tests call
`NewMySensor` directly with a typed config. Leave
`newMySensorModuleMySensor` alone and update `NewMySensor` to initialize any
state your module needs:

```go
func NewMySensor(ctx context.Context, deps resource.Dependencies, name resource.Name, conf *Config, logger logging.Logger) (sensor.Sensor, error) {
    cancelCtx, cancelFunc := context.WithCancel(context.Background())

    timeout := time.Duration(conf.PollInterval) * time.Second
    if timeout == 0 {
        timeout = 10 * time.Second
    }

    s := &mySensorModuleMySensor{
        name:       name,
        logger:     logger,
        cfg:        conf,
        cancelCtx:  cancelCtx,
        cancelFunc: cancelFunc,
        client:     &http.Client{Timeout: timeout},
    }
    return s, nil
}
```
Add fields to the generated struct for any state your module needs at runtime:

```go
type mySensorModuleMySensor struct {
    resource.AlwaysRebuild

    name resource.Name

    logger logging.Logger
    cfg    *Config

    cancelCtx  context.Context
    cancelFunc func()
    client     *http.Client
}
```
Leave the generated `resource.AlwaysRebuild` embed in place. The generated
`Name()`, `Close()`, `DoCommand()`, and `Status()` methods also stay. `Close`
calls `cancelFunc()` to stop any background work. `DoCommand` and `Status`
are stubs returning `fmt.Errorf("not implemented")`. Leave them as-is unless
your module needs custom handling.



#### Implement the API method

For a sensor, the key method is `GetReadings`, which returns a map of reading
names to values. This is the method that data capture calls and your application
code queries.

The generator creates a stub that returns an error. Replace it with your
implementation:




### Python

Add a `get_readings` method to your class. The return type is
`Mapping[str, SensorReading]` (import `SensorReading` from `viam.utils`):

```python
async def get_readings(
        self,
        *,
        extra: Optional[Mapping[str, Any]] = None,
        timeout: Optional[float] = None,
        **kwargs,
    ) -> Mapping[str, SensorReading]:
        try:
            response = requests.get(self.source_url, timeout=5)
            response.raise_for_status()
            data = response.json()
            return {
                "temperature": data["temp"],
                "humidity": data["humidity"],
            }
        except requests.RequestException as e:
            self.logger.error(f"Failed to read from {self.source_url}: {e}")
            raise
```

### Go

Find the `Readings` method stub and replace it:

```go
type sensorResponse struct {
    Temp     float64 `json:"temp"`
    Humidity float64 `json:"humidity"`
}

func (s *mySensorModuleMySensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.cfg.SourceURL, nil)
    if err != nil {
        return nil, fmt.Errorf("building request: %w", err)
    }
    resp, err := s.client.Do(req)
    if err != nil {
        s.logger.CErrorw(ctx, "failed to read from source",
            "url", s.cfg.SourceURL, "error", err)
        return nil, fmt.Errorf("failed to read from %s: %w", s.cfg.SourceURL, err)
    }
    defer resp.Body.Close()

    var data sensorResponse
    if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
        return nil, fmt.Errorf("failed to decode response: %w", err)
    }

    return map[string]interface{}{
        "temperature": data.Temp,
        "humidity":    data.Humidity,
    }, nil
}
```



**View the complete resource file**



For reference, here is the complete resource file after all the changes above.
`my-org` in these samples stands in for the namespace you entered when running
the generator.




### Python

`src/models/my_sensor.py`:

```python
from typing import (Any, ClassVar, Dict, Final, List, Mapping, Optional,
                    Sequence, Tuple)

import requests
from typing_extensions import Self
from viam.components.sensor import *
from viam.proto.app.robot import ComponentConfig
from viam.proto.common import Geometry, ResourceName
from viam.resource.base import ResourceBase
from viam.resource.easy_resource import EasyResource
from viam.resource.types import Model, ModelFamily
from viam.utils import SensorReading, ValueTypes

class MySensor(Sensor, EasyResource):
    # To enable debug-level logging, either run viam-server with the --debug option,
    # or configure your resource/machine to display debug logs.
    MODEL: ClassVar[Model] = Model(
        ModelFamily("my-org", "my-sensor-module"), "my-sensor"
    )

    source_url: str
    poll_interval: float

    @classmethod
    def new(
        cls, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]
    ) -> Self:
        """This method creates a new instance of this Sensor component.
        The default implementation sets the name from the `config` parameter.

        Args:
            config (ComponentConfig): The configuration for this resource
            dependencies (Mapping[ResourceName, ResourceBase]): The dependencies (both required and optional)

        Returns:
            Self: The resource
        """
        sensor = super().new(config, dependencies)
        fields = config.attributes.fields
        sensor.source_url = fields["source_url"].string_value
        sensor.poll_interval = (
            fields["poll_interval"].number_value
            if "poll_interval" in fields
            else 10.0
        )
        return sensor

    @classmethod
    def validate_config(
        cls, config: ComponentConfig
    ) -> Tuple[Sequence[str], Sequence[str]]:
        """This method allows you to validate the configuration object received from the machine,
        as well as to return any required dependencies or optional dependencies based on that `config`.

        Args:
            config (ComponentConfig): The configuration for this resource

        Returns:
            Tuple[Sequence[str], Sequence[str]]: A tuple where the
                first element is a list of required dependencies and the
                second element is a list of optional dependencies
        """
        fields = config.attributes.fields
        if "source_url" not in fields:
            raise Exception("source_url is required")
        if not fields["source_url"].string_value.startswith("http"):
            raise Exception("source_url must be an HTTP or HTTPS URL")
        return [], []

    async def get_readings(
        self,
        *,
        extra: Optional[Mapping[str, Any]] = None,
        timeout: Optional[float] = None,
        **kwargs
    ) -> Mapping[str, SensorReading]:
        try:
            response = requests.get(self.source_url, timeout=5)
            response.raise_for_status()
            data = response.json()
            return {
                "temperature": data["temp"],
                "humidity": data["humidity"],
            }
        except requests.RequestException as e:
            self.logger.error(f"Failed to read from {self.source_url}: {e}")
            raise

    async def do_command(
        self,
        command: Mapping[str, ValueTypes],
        *,
        timeout: Optional[float] = None,
        **kwargs
    ) -> Mapping[str, ValueTypes]:
        self.logger.error("`do_command` is not implemented")
        raise NotImplementedError()

    async def get_status(
        self, *, timeout: Optional[float] = None, **kwargs
    ) -> Mapping[str, ValueTypes]:
        self.logger.error("`get_status` is not implemented")
        raise NotImplementedError()

    async def get_geometries(
        self, *, extra: Optional[Dict[str, Any]] = None, timeout: Optional[float] = None
    ) -> Sequence[Geometry]:
        self.logger.error("`get_geometries` is not implemented")
        raise NotImplementedError()
```

### Go

`module.go`:

```go
package mysensormodule

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
    "time"

    sensor "go.viam.com/rdk/components/sensor"
    "go.viam.com/rdk/logging"
    "go.viam.com/rdk/resource"
)

var (
    MySensor         = resource.NewModel("my-org", "my-sensor-module", "my-sensor")
    errUnimplemented = errors.New("unimplemented")
)

type Config struct {
    SourceURL    string  `json:"source_url"`
    PollInterval float64 `json:"poll_interval"`
}

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

func init() {
    resource.RegisterComponent(sensor.API, MySensor,
        resource.Registration[sensor.Sensor, *Config]{
            Constructor: newMySensorModuleMySensor,
        },
    )
}

type mySensorModuleMySensor struct {
    resource.AlwaysRebuild

    name resource.Name

    logger logging.Logger
    cfg    *Config

    cancelCtx  context.Context
    cancelFunc func()
    client     *http.Client
}

func newMySensorModuleMySensor(ctx context.Context, deps resource.Dependencies, rawConf resource.Config, logger logging.Logger) (sensor.Sensor, error) {
    conf, err := resource.NativeConfig[*Config](rawConf)
    if err != nil {
        return nil, err
    }
    return NewMySensor(ctx, deps, rawConf.ResourceName(), conf, logger)
}

func NewMySensor(ctx context.Context, deps resource.Dependencies, name resource.Name, conf *Config, logger logging.Logger) (sensor.Sensor, error) {
    cancelCtx, cancelFunc := context.WithCancel(context.Background())

    timeout := time.Duration(conf.PollInterval) * time.Second
    if timeout == 0 {
        timeout = 10 * time.Second
    }

    s := &mySensorModuleMySensor{
        name:       name,
        logger:     logger,
        cfg:        conf,
        cancelCtx:  cancelCtx,
        cancelFunc: cancelFunc,
        client:     &http.Client{Timeout: timeout},
    }
    return s, nil
}

func (s *mySensorModuleMySensor) Name() resource.Name {
    return s.name
}

type sensorResponse struct {
    Temp     float64 `json:"temp"`
    Humidity float64 `json:"humidity"`
}

func (s *mySensorModuleMySensor) Readings(ctx context.Context, extra map[string]interface{}) (map[string]interface{}, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.cfg.SourceURL, nil)
    if err != nil {
        return nil, fmt.Errorf("building request: %w", err)
    }
    resp, err := s.client.Do(req)
    if err != nil {
        s.logger.CErrorw(ctx, "failed to read from source",
            "url", s.cfg.SourceURL, "error", err)
        return nil, fmt.Errorf("failed to read from %s: %w", s.cfg.SourceURL, err)
    }
    defer resp.Body.Close()

    var data sensorResponse
    if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
        return nil, fmt.Errorf("failed to decode response: %w", err)
    }

    return map[string]interface{}{
        "temperature": data.Temp,
        "humidity":    data.Humidity,
    }, nil
}

func (s *mySensorModuleMySensor) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) {
    return nil, fmt.Errorf("not implemented")
}

func (s *mySensorModuleMySensor) Status(ctx context.Context) (map[string]interface{}, error) {
    return nil, fmt.Errorf("not implemented")
}

func (s *mySensorModuleMySensor) Close(context.Context) error {
    // Put close code here
    s.cancelFunc()
    return nil
}
```






#### Understanding the entry point

The generator also creates the entry point file that `viam-server` launches.
You typically do not need to modify it.




### Python

`src/main.py`:

```python
import asyncio
from viam.module.module import Module
from models.my_sensor import MySensor as MySensorModel

if __name__ == '__main__':
    asyncio.run(Module.run_from_registry())
```
`run_from_registry()` automatically discovers all imported resource classes and
registers them with `viam-server`. If you add more models to your module, import
them here.

### Go

`cmd/module/main.go`:

```go
package main

import (
    "mysensormodule"

    "go.viam.com/rdk/module"
    "go.viam.com/rdk/resource"
    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{sensor.API, mysensormodule.MySensor})
}
```
`ModularMain` handles socket parsing, signal handling, and graceful shutdown.
The import of the resource package triggers its `init()` function, which calls
`resource.RegisterComponent` to register the model. If you add more models,
add more `resource.APIModel` entries to the `ModularMain` call.



### 3. Test locally

Use the CLI to build and deploy your module to a machine, then verify it works. Two commands cover the common development loop: `viam module reload` (cloud build) for cross-architecture work, and `viam module reload-local` (local build) for same-architecture iteration. Use `reload` when developing on a different architecture than your target, for example on macOS deploying to a Raspberry Pi. Use `reload-local` when architectures match for faster iteration.

**Deploy with hot reloading:**

First, find your machine's part ID. At the top of the machine's page, click the **Live** / **Offline** status dropdown, then click **Part ID** to copy it. In the commands below, `--model-name` adds an instance of your model to the machine config so you don't have to create it by hand, and `--resource-name` names that instance. Run these commands from your module's root directory (where `meta.json` lives). If you need to invoke them from elsewhere, pass `--module <path/to/meta.json>`.

```bash
# Build in the cloud, deploy, and add a component named `my-sensor-1`
viam module reload --part-id <machine-part-id> \
  --model-name my-org:my-sensor-module:my-sensor --resource-name my-sensor-1
```

```bash
# Build locally (same-architecture only), transfer, and add the component
viam module reload-local --part-id <machine-part-id> \
  --model-name my-org:my-sensor-module:my-sensor --resource-name my-sensor-1
```

After the first reload succeeds, open the machine's **CONFIGURE** tab and set your new sensor's attributes. Replace `source_url` with a real endpoint that returns JSON matching the shape your `Readings` implementation expects (the example reads `temp` and `humidity` keys from the response body):

```json
{
  "source_url": "https://your-endpoint.example.com/readings"
}
```

Click **Save**.

**Test using the Test section:**

1. Find your sensor component and expand the **Test** section.
2. Your temperature and humidity values appear automatically under
   **GetReadings**.

**Get a ready-to-run code sample:**

The **CONNECT** tab on your machine's page in the Viam app provides generated
code samples in Python and Go that connect to your machine and access all
configured components. Use this as a starting point for application code that
interacts with your module.

**Rebuild and redeploy during development:**

Each time you make changes, run `viam module reload` (or `reload-local`) again. Use `--no-build` to skip the build step if you already built manually. Use `viam module restart` to restart without rebuilding (for example, after editing Python source).

### 4. Add logging

Both the Python and Go SDKs provide a logger that writes to `viam-server`'s log
stream, visible in the **LOGS** tab.




### Python

```python
self.logger.info("Sensor initialized with source URL: %s", self.source_url)
self.logger.debug("Raw response from source: %s", data)
self.logger.warning("Source returned unexpected field: %s", field_name)
self.logger.error("Failed to connect to source: %s", error)
```

### Go

```go
s.logger.CInfof(ctx, "Sensor initialized with source URL: %s", s.cfg.SourceURL)
s.logger.CDebugf(ctx, "Raw response from source: %v", data)
s.logger.CWarnw(ctx, "Source returned unexpected field", "field", fieldName)
s.logger.CErrorw(ctx, "Failed to connect to source", "error", err)
```



Use `info` for significant events, `debug` for detailed data, `warning` for
recoverable problems, and `error` for failures.

### 5. Handle dependencies

Many modules need access to other resources on the same machine. To use
another resource, you need to do three things:

1. **Declare the dependency** in your config validation method by returning the
   resource name in the required (or optional) dependencies list.
2. **Resolve the dependency** in your constructor by looking it up from the
   `dependencies` map that `viam-server` passes in.
3. **Call methods on it** in your API implementation, just like any other
   typed resource.

The following example shows all three. It implements a sensor that depends on
another sensor -- it reads Celsius temperature readings from the source sensor
and converts them to Fahrenheit. Watch for the numbered comments in the code.

In Go, the example uses `sensor.FromProvider(deps, name)` to resolve the dependency. It returns a typed `sensor.Sensor` handle, so your code does not need a type assertion. Every component and service package in the SDK exposes its own `FromProvider` helper. For a motor dependency, use `motor.FromProvider(deps, name)`. For a camera, use `camera.FromProvider(deps, name)`. And so on.

The Python SDK has no equivalent helper. Iterate the `dependencies` map and match by `ResourceName.name`, as the example below shows.

Place `TempConverter` alongside `MySensor`. In Python, add it as a new file under `src/models/` (for example, `src/models/temp_converter.py`). In Go, put it in the same package as `MySensor`, either by extending `module.go` or adding another `.go` file in the same directory. For registration, see [Step 7](#7-add-multiple-models-to-one-module-optional).




### Python

```python
class TempConverter(Sensor, EasyResource):
    MODEL: ClassVar[Model] = Model(
        ModelFamily("my-org", "my-sensor-module"), "temp-converter"
    )

    source_sensor: Sensor

    @classmethod
    def validate_config(
        cls, config: ComponentConfig
    ) -> Tuple[Sequence[str], Sequence[str]]:
        fields = config.attributes.fields
        if "source_sensor" not in fields:
            raise Exception("source_sensor is required")
        source = fields["source_sensor"].string_value
        # 1. Declare: return the source sensor name as a required dependency
        return [source], []

    @classmethod
    def new(cls, config: ComponentConfig,
            dependencies: Mapping[ResourceName, ResourceBase]) -> Self:
        instance = super().new(config, dependencies)
        source_name = config.attributes.fields["source_sensor"].string_value
        # 2. Resolve: find the dependency in the map viam-server passes in
        for name, dep in dependencies.items():
            if name.name == source_name:
                instance.source_sensor = dep
                break
        return instance

    async def get_readings(self, *, extra=None, timeout=None,
                           **kwargs) -> Mapping[str, SensorReading]:
        # 3. Use: call methods on the dependency like any typed resource
        readings = await self.source_sensor.get_readings()
        celsius = readings["temperature"]
        return {"temperature_f": celsius * 9.0 / 5.0 + 32.0}
```

### Go

```go
var TempConverter = resource.NewModel("my-org", "my-sensor-module", "temp-converter")

func init() {
    resource.RegisterComponent(sensor.API, TempConverter,
        resource.Registration[sensor.Sensor, *ConverterConfig]{
            Constructor: newMySensorModuleTempConverter,
        },
    )
}

type ConverterConfig struct {
    SourceSensor string `json:"source_sensor"`
}

func (cfg *ConverterConfig) Validate(path string) ([]string, []string, error) {
    if cfg.SourceSensor == "" {
        return nil, nil, fmt.Errorf("source_sensor is required")
    }
    // 1. Declare: return the source sensor name as a required dependency
    return []string{cfg.SourceSensor}, nil, nil
}

type mySensorModuleTempConverter struct {
    resource.AlwaysRebuild

    name resource.Name

    logger logging.Logger
    cfg    *ConverterConfig

    cancelCtx  context.Context
    cancelFunc func()
    source     sensor.Sensor
}

func newMySensorModuleTempConverter(
    ctx context.Context,
    deps resource.Dependencies,
    rawConf resource.Config,
    logger logging.Logger,
) (sensor.Sensor, error) {
    conf, err := resource.NativeConfig[*ConverterConfig](rawConf)
    if err != nil {
        return nil, err
    }
    return NewTempConverter(ctx, deps, rawConf.ResourceName(), conf, logger)
}

func NewTempConverter(
    ctx context.Context,
    deps resource.Dependencies,
    name resource.Name,
    conf *ConverterConfig,
    logger logging.Logger,
) (sensor.Sensor, error) {
    cancelCtx, cancelFunc := context.WithCancel(context.Background())

    // 2. Resolve: look up the dependency by name from the map viam-server passes in
    src, err := sensor.FromProvider(deps, conf.SourceSensor)
    if err != nil {
        cancelFunc()
        return nil, fmt.Errorf("source sensor %q not found: %w",
            conf.SourceSensor, err)
    }

    return &mySensorModuleTempConverter{
        name:       name,
        logger:     logger,
        cfg:        conf,
        cancelCtx:  cancelCtx,
        cancelFunc: cancelFunc,
        source:     src,
    }, nil
}

func (s *mySensorModuleTempConverter) Name() resource.Name {
    return s.name
}

func (s *mySensorModuleTempConverter) Readings(
    ctx context.Context,
    extra map[string]interface{},
) (map[string]interface{}, error) {
    // 3. Use: call methods on the dependency like any typed resource
    readings, err := s.source.Readings(ctx, extra)
    if err != nil {
        return nil, fmt.Errorf("failed to read source sensor: %w", err)
    }
    celsius, ok := readings["temperature"].(float64)
    if !ok {
        return nil, fmt.Errorf("source sensor did not return a temperature reading")
    }
    return map[string]interface{}{
        "temperature_f": celsius*9.0/5.0 + 32.0,
    }, nil
}

func (s *mySensorModuleTempConverter) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) {
    return nil, fmt.Errorf("not implemented")
}

func (s *mySensorModuleTempConverter) Status(ctx context.Context) (map[string]interface{}, error) {
    return nil, fmt.Errorf("not implemented")
}

func (s *mySensorModuleTempConverter) Close(ctx context.Context) error {
    s.cancelFunc()
    return nil
}
```



`TempConverter` is a second model in the same module. To register it alongside your first model, see [Step 7](#7-add-multiple-models-to-one-module-optional).

### 6. Use the module data directory

Every module gets a persistent data directory at the path specified by the
`VIAM_MODULE_DATA` environment variable. Use this for caches, databases, or
any state that should survive module restarts.




### Python

```python
import os

data_dir = os.environ.get("VIAM_MODULE_DATA", "/tmp")
cache_path = os.path.join(data_dir, "readings_cache.json")
```

### Go

```go
dataDir := os.Getenv("VIAM_MODULE_DATA")
cachePath := filepath.Join(dataDir, "readings_cache.json")
```



The directory is created automatically by `viam-server` at
`$VIAM_HOME/module-data/<machine-id>/<module-name>/` (where `VIAM_HOME`
defaults to `~/.viam`) and persists across module
restarts and reconfigurations.

### 7. Add multiple models to one module (optional)

A single module can provide multiple models, even across different APIs
(for example, a sensor and a camera). There is no limit on the number of models
per module.

To add a second model, run [`viam module add-model`](/cli/reference/#module-add-model) from within the module directory:

```sh {class="command-line" data-prompt="$"}
viam module add-model --resource-subtype=sensor --model-name=my-sensor
```

The command generates the model source file, registers it in `meta.json`, and updates the entry point to import the new model.

You can also add a model manually:




### Python

Import the new model in `src/main.py`. `run_from_registry()` automatically
discovers all imported resource classes:

```python
import asyncio
from viam.module.module import Module
from models.my_sensor import MySensor as MySensorModel
from models.my_camera import MyCamera as MyCameraModel

if __name__ == '__main__':
    asyncio.run(Module.run_from_registry())
```

### Go

Add more `resource.APIModel` entries to `ModularMain`:

```go
package main

import (
    "mysensormodule"

    "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{sensor.API, mysensormodule.MySensor},
        resource.APIModel{camera.API, mysensormodule.MyCamera},
    )
}
```
Each model needs its own `init()` function calling `resource.RegisterComponent`
(or `resource.RegisterService`) with its API, model, and constructor.



4. Delete the temporary generated directory.
5. Update `meta.json` to list all models (or use `viam module update-models
--binary ./bin/module` to detect them automatically from a Go binary).

## Try It

1. Generate a sensor module using `viam module generate`.
2. Open the generated resource file and implement your config validation and
   `GetReadings` method.
3. Configure the module on your machine as a local module.
4. Expand the **Test** section on your sensor. Verify readings appear
   automatically under **GetReadings**.
5. [Enable data capture](/data/capture-sync/capture-and-sync-data/) on the
   sensor. Wait one minute, then check the **DATA** tab to confirm readings
   are flowing to the cloud.
6. Add a new key to the readings map (for example, `"pressure": 1013.25`).
   Rebuild and redeploy with `viam module reload-local`. Verify the new reading
   appears on the Test section.

## Troubleshooting

**Module crashes on startup**



- Check the **LOGS** tab for the crash traceback. The most common cause is a
  missing dependency -- a Python import not in `requirements.txt` or a Go
  package not in `go.mod`.
- For Python, verify the module runs outside of `viam-server`:
  `python3 -m src.main` (from your module directory, with the virtualenv
  activated).
- For Go, verify the binary runs: `./bin/<your-module-name>` (the output path
  is set in your `Makefile`).




**Module times out on startup**



`viam-server` expects the module to complete startup within 5 minutes (the
default `VIAM_MODULE_STARTUP_TIMEOUT`). If your module does heavy
initialization (loading large files, connecting to slow services), it may
time out.

- Move slow initialization out of `init()` or model registration and into the
  constructor instead, where it runs per-resource rather than blocking module
  startup.
- Check the **LOGS** tab for timeout errors.




**Dependency not found**



- Confirm the dependency name returned by your config validation method matches
  the resource name on the machine exactly (names are compared as strings, so
  case and spelling must match).
- Verify the depended-on resource exists and is configured correctly.
- Check for circular dependencies -- if A depends on B and B depends on A,
  both will fail to start. Check the **LOGS** tab for "circular dependency"
  errors.




**Readings returning None or nil**



- Add logging inside your `GetReadings` implementation to see what data your
  source returns.
- `GetReadings` must return a non-nil map. If it returns `nil` (Go) or `None`
  (Python), `viam-server` treats this as an error.
- Check network connectivity from the machine if your sensor reads from an
  external source.




**Module not restarting after code changes**



`viam-server` does not watch your module's source files or binary for changes.
To deploy changes:

- Use `viam module reload-local --part-id <id>` to rebuild and redeploy.
- Use `viam module restart --part-id <id>` to restart without rebuilding.

If `reload-local` fails:

- **"no build command"** -- Your `meta.json` is missing a `build.build` field.
  Add the path to your build script (for example, `"build": "./build.sh"`).
- **"could not find module ID"** -- Run the command from the directory
  containing `meta.json`, or pass `--module <path>` to point to your
  `meta.json` file.
- **Wrong destination directory** -- The CLI asks the target machine for
  its Viam home directory automatically. If that query fails or returns a
  wrong path, pass `--home <path>` to override it.




**Data capture not recording readings**



Data capture requires both the data management service and a per-resource
capture configuration:

- Verify the data management service is configured and does not have
  `capture_disabled` set to `true`.
- Verify your sensor component has a data capture configuration with
  `capture_frequency_hz` greater than `0`.
- Check that `GetReadings` returns a valid, non-nil map.
- If the capture frequency is very low, you may need to wait longer to see
  data appear on the [**Data** page](https://app.viam.com/data).




## What's Next

- [Write a Logic Module](/build-modules/write-a-logic-module/) -- write a module
  that monitors sensors, coordinates components, or runs automation logic.
- [Deploy a Module](/build-modules/deploy-a-module/) -- package your module
  and upload it to the Viam registry for distribution.
- [Module Reference](/build-modules/module-reference/) -- complete reference
  for meta.json, CLI commands, environment variables, and resource interfaces.

