Previous
Use registry modules
Viam provides built-in support for many types of hardware and software, but you may want to use hardware that Viam doesn’t support out of the box, or add application-specific logic. Modules let you add that support yourself.
An inline module is the fastest way to get started. You write your module code directly in the Viam app’s browser-based editor – no IDE, terminal, or GitHub account required. When you click Save & Deploy, Viam builds your module in the cloud and deploys it to your machine automatically.
An inline module is a Viam-hosted module. Viam manages the source code, versioning, builds, and deployment for you. You edit a single source file in the browser, and Viam handles everything else:
| Inline (Viam-hosted) | Externally managed | |
|---|---|---|
| Where you write code | Browser editor in the Viam app | Your own IDE, locally or in a repo |
| Source control | Managed by Viam | Your own git repository |
| Build system | Automatic cloud builds on save | CLI upload or GitHub Actions |
| Versioning | Automatic (0.0.1, 0.0.2, …) | You choose semantic versions |
| Visibility | Private to your organization | Private or public |
| Best for | Prototyping, simple control logic, no-toolchain setups | Production modules, public distribution, complex dependencies |
Both types run identically at runtime – as child processes communicating with
viam-server over gRPC. The difference is how you create, edit, and deploy the
module.
If you want to manage your own source code and build pipeline, see Write a Driver Module or Write a Logic Module instead.
Inline modules create a Generic service (not a component). The Generic
service provides a single method – DoCommand – that accepts an arbitrary
JSON command and returns a JSON response. Your control logic goes inside
DoCommand.
Use the Generic service when:
servo-distance-control) and choose a language
(Python or Go).The browser opens the code editor with a working template that includes all necessary imports and method stubs.
The editor opens a single file – your module’s main source file. The template includes three methods you need to fill in:
The editable file is src/models/generic_service.py. It contains a class that
extends GenericService and EasyResource:
class MyGenericService(GenericService, EasyResource):
MODEL: ClassVar[Model] = Model(
ModelFamily("my-org", "my-module"), "generic-service"
)
The three methods to implement:
validate_config – check that configuration attributes are valid and
declare dependencies.new – initialize your service with attributes and dependencies.do_command – your control logic.Do not change the class name or the MODEL triplet. Viam uses these
auto-generated values to identify your module. Changing them will break your
inline module.
The editable file is module.go. It contains a struct, a config type, and
registration logic:
var GenericService = resource.NewModel(
"my-org", "my-module", "generic-service",
)
func init() {
resource.RegisterService(genericservice.API, GenericService,
resource.Registration[resource.Resource, *Config]{
Constructor: newGenericService,
},
)
}
The three areas to implement:
Validate on the Config struct – check attributes, return
dependencies.NewGenericService – initialize the service with attributes and
dependencies.DoCommand – your control logic.Do not change the model name triplet, struct names, or public function names. Viam uses these auto-generated values to identify your module. Changing them will break your inline module.
The validate method runs every time the machine configuration changes. It checks that the attributes passed to your service are valid and declares dependencies on other components or services.
This example validates attributes for a distance-responsive servo controller – a service that reads an ultrasonic sensor and adjusts a servo angle based on distance:
@classmethod
def validate_config(
cls, config: ComponentConfig
) -> Tuple[Sequence[str], Sequence[str]]:
attrs = struct_to_dict(config.attributes)
# Required numeric attributes
sensor_range_start = attrs.get("sensor_range_start")
if sensor_range_start is None or not isinstance(
sensor_range_start, (int, float)
):
raise ValueError(
"attribute 'sensor_range_start' is required "
"and must be an int or float value"
)
sensor_range_end = attrs.get("sensor_range_end")
if sensor_range_end is None or not isinstance(
sensor_range_end, (int, float)
):
raise ValueError(
"attribute 'sensor_range_end' is required "
"and must be an int or float value"
)
# Required dependency attributes
required_deps: List[str] = []
servo_name = attrs.get("servo")
if not isinstance(servo_name, str) or not servo_name:
raise ValueError(
"attribute 'servo' (non-empty string) is required"
)
required_deps.append(servo_name)
sensor_name = attrs.get("sensor")
if not isinstance(sensor_name, str) or not sensor_name:
raise ValueError(
"attribute 'sensor' (non-empty string) is required"
)
required_deps.append(sensor_name)
return required_deps, []
The return value is a tuple of two lists:
type Config struct {
SensorRangeStart float64 `json:"sensor_range_start"`
SensorRangeEnd float64 `json:"sensor_range_end"`
ServoAngleMin *int64 `json:"servo_angle_min"`
ServoAngleMax *int64 `json:"servo_angle_max"`
Reversed *bool `json:"reversed"`
Servo string `json:"servo"`
Sensor string `json:"sensor"`
}
func (cfg *Config) Validate(path string) ([]string, []string, error) {
if cfg.SensorRangeStart == 0 {
return nil, nil, fmt.Errorf(
"%s: 'sensor_range_start' is required and must be non-zero",
path,
)
}
if cfg.SensorRangeEnd == 0 {
return nil, nil, fmt.Errorf(
"%s: 'sensor_range_end' is required and must be non-zero",
path,
)
}
requiredDeps := []string{}
if cfg.Servo == "" {
return nil, nil, fmt.Errorf(
"%s: 'servo' (non-empty string) is required", path,
)
}
requiredDeps = append(requiredDeps, cfg.Servo)
if cfg.Sensor == "" {
return nil, nil, fmt.Errorf(
"%s: 'sensor' (non-empty string) is required", path,
)
}
requiredDeps = append(requiredDeps, cfg.Sensor)
return requiredDeps, []string{}, nil
}
The Validate method returns two slices (required dependencies and optional
dependencies) and an error.
The constructor runs when the service is first created and again whenever its configuration changes. Use it to parse attributes and resolve dependencies.
@classmethod
def new(
cls, config: ComponentConfig,
dependencies: Mapping[ResourceName, ResourceBase]
) -> Self:
attrs = struct_to_dict(config.attributes)
self = cls(config.name)
# Required attributes
self.sensor_range_start = float(attrs.get("sensor_range_start"))
self.sensor_range_end = float(attrs.get("sensor_range_end"))
# Optional attributes with defaults
self.servo_angle_min = float(attrs.get("servo_angle_min", 0))
self.servo_angle_max = float(attrs.get("servo_angle_max", 180))
self.reversed = attrs.get("reversed", False)
# Resolve dependencies
servo_name = attrs.get("servo")
sensor_name = attrs.get("sensor")
self.servo = dependencies[Servo.get_resource_name(servo_name)]
self.sensor = dependencies[Sensor.get_resource_name(sensor_name)]
return self
func NewGenericService(
ctx context.Context, deps resource.Dependencies,
name resource.Name, conf *Config, logger logging.Logger,
) (resource.Resource, error) {
cancelCtx, cancelFunc := context.WithCancel(context.Background())
// Apply defaults for optional fields
servoAngleMin := int64(0)
if conf.ServoAngleMin != nil {
servoAngleMin = *conf.ServoAngleMin
}
servoAngleMax := int64(180)
if conf.ServoAngleMax != nil {
servoAngleMax = *conf.ServoAngleMax
}
reversed := false
if conf.Reversed != nil {
reversed = *conf.Reversed
}
// Resolve dependencies
servoDep, err := servo.FromProvider(deps, conf.Servo)
if err != nil {
return nil, err
}
sensorDep, err := sensor.FromProvider(deps, conf.Sensor)
if err != nil {
return nil, err
}
return &genericService{
name: name,
logger: logger,
cfg: conf,
cancelCtx: cancelCtx,
cancelFunc: cancelFunc,
servo: servoDep,
sensor: sensorDep,
sensorRangeStart: conf.SensorRangeStart,
sensorRangeEnd: conf.SensorRangeEnd,
servoAngleMin: servoAngleMin,
servoAngleMax: servoAngleMax,
reversed: reversed,
}, nil
}
DoCommand is where your control logic goes. This example reads a distance
sensor and maps the reading to a servo angle:
async def do_command(
self, command: Mapping[str, ValueTypes], *,
timeout: Optional[float] = None, **kwargs
) -> Mapping[str, ValueTypes]:
readings = await self.sensor.get_readings()
if not readings:
raise ValueError("No sensor readings available")
value = next(iter(readings.values()))
# Map sensor range to servo angle range
t = (value - self.sensor_range_start) / (
self.sensor_range_end - self.sensor_range_start
)
t = max(0.0, min(1.0, 1.0 - t if self.reversed else t))
angle = self.servo_angle_min + t * (
self.servo_angle_max - self.servo_angle_min
)
await self.servo.move(int(angle))
return {"servo_angle_deg": angle}
func (s *genericService) DoCommand(
ctx context.Context, cmd map[string]interface{},
) (map[string]interface{}, error) {
readings, err := s.sensor.Readings(ctx, nil)
if err != nil {
return nil, err
}
value, ok := readings["distance"].(float64)
if !ok {
return nil, fmt.Errorf("sensor reading 'distance' must be a float64")
}
// Map sensor range to servo angle range
t := (value - s.sensorRangeStart) /
(s.sensorRangeEnd - s.sensorRangeStart)
if t < 0 { t = 0 } else if t > 1 { t = 1 }
if s.reversed { t = 1 - t }
angle := float64(s.servoAngleMin) +
t * (float64(s.servoAngleMax) - float64(s.servoAngleMin))
return map[string]interface{}{
"servo_angle_deg": angle,
}, s.servo.Move(ctx, uint32(angle), nil)
}
In this example no DoCommand payload is used. You can use the command payload to customize behavior per invocation. Attributes are constant across all invocations; the DoCommand payload can vary with each call.
Each save creates a new version in your module’s history. You can switch between versions using the version dropdown in the editor toolbar.
The Viam app navigates you to the machine’s CONFIGURE tab with your module added.
{
"sensor_range_start": 0.05,
"sensor_range_end": 0.3,
"servo_angle_min": 40,
"servo_angle_max": 270,
"reversed": true,
"servo": "servo-1",
"sensor": "sensor-1"
}
{} if your DoCommand does not use the
payload):
{}
{ "servo_angle_deg": 155.0 }
The DoCommand section in the Viam app runs your logic once per click. To have it run automatically:
validate_config, the constructor, and do_command.Was this page helpful?
Glad to hear it! If you have any other feedback please let us know:
We're sorry about that. To help us improve, please tell us what we can do better:
Thank you!