Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions examples/complex_module/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# VIAM Complex Module Example

This example goes through how to create custom modular resources using Viam's python SDK, and how to connect it to a Robot.

This is a limited document. For a more in-depth understanding of modules, see the [documentation](https://docs.viam.com/program/extend/modular-resources/).

## Purpose

Modular resources allow you to define custom components and services, and add them to your robot. Viam ships with many component types, but you're not limited to only using those types -- you can create your own using modules.

For more information, see the [documentation](https://docs.viam.com/program/extend/modular-resources/). For a simpler example, take a look at the [simple module example](https://github.com/viamrobotics/viam-python-sdk/tree/main/examples/simple_module), which only contains one custom resource model in one file.

## Project structure

The definition of the new resources are in the `src` directory. Within this directory are the `proto`, `gizmo`, `arm`, and `summation` subdirectories.

The `proto` directory contains the `gizmo.proto` and `summation.proto` definitions of all the message types and calls that can be made to the Gizmo component and Summation service. It also has the compiled python output of the protobuf definition.
Expand All @@ -19,18 +22,22 @@ Similarly, the `summation` directory contains the analogous definitions for the

The `arm` directory contains all the necessary definitions for creating a custom modular `Arm` component type. Since it is subclassing an already existing component supported by the Viam SDK, there is no need for an `api.py` file. For a more in-depth tutorial on how to write a modular component from an existing resource, see the [documentation](https://python.viam.dev/examples/example.html#create-custom-modules).

The `base` directory contains all the necessary definitions for creating a custom modular `Base` component type. Like the previous `Arm` implementation, the `base` directory is subclassing an already existing component supported by the Viam SDK, so an `api.py` is not necessary. A more in-depth tutorial on how to write custom modular components from existing resources can be found in the [documentation](https://python.viam.dev/examples/example.html#create-custom-modules).

There is also a `main.py` file, which creates a module, adds the desired resources, and starts the module. This file is called by the `run.sh` script, which is the entrypoint for this module. Read further to learn how to connect this module to your robot.

Outside the `src` directory, there is a `client.py` file. You can use this file to test the module once you have connected to your robot and configured the module. You will have to update the credentials and robot address in that file.

## Configuring and using the module

These steps assume that you have a robot available at [app.viam.com](app.viam.com).

The `run.sh` script is the entrypoint for this module. To connect this module with your robot, you must add this module's entrypoint to the robot's config. For example, the entrypoint file may be at `/home/viam-python-sdk/examples/complex_module/run.sh` and you must add this file path to your configuration. See the [documentation](https://docs.viam.com/program/extend/modular-resources/#use-a-modular-resource-with-your-robot) for more details.

Once the module has been added to your robot, add a `Gizmo` component that uses the `MyGizmo` model. See the [documentation](https://docs.viam.com/program/extend/modular-resources/#configure-a-component-instance-for-a-modular-resource) for more details. You can also add an `Arm` component that uses the `MyArm` model and a `Summation` service that uses the `MySum` model in a similar manner.

An example configuration for an Arm component, a Gizmo component, and a Summation service could look like this:

```json
{
"components": [
Expand Down Expand Up @@ -64,6 +71,29 @@ An example configuration for an Arm component, a Gizmo component, and a Summatio
"board": ""
},
"depends_on": []
},
{
"name": "motor2",
"type": "motor",
"model": "fake",
"attributes": {
"pins": {
"dir": "",
"pwm": ""
},
"board": ""
},
"depends_on": []
},
{
"name": "base1",
"type": "base",
"attributes": {
"left": "motor1",
"right": "motor2"
},
"model": "acme:demo:mybase",
"depends_on": []
}
],
"services": [
Expand Down
11 changes: 9 additions & 2 deletions examples/complex_module/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from viam import logging
from viam.robot.client import RobotClient
from viam.rpc.dial import Credentials, DialOptions
from viam.components.base import Base


async def connect():
Expand All @@ -18,7 +19,8 @@ async def main():
robot = await connect()

print("Resources:")
print(robot.resource_names)
for resource in robot.resource_names:
print(resource)

# ####### GIZMO ####### #
gizmo = Gizmo.from_robot(robot, name="gizmo1")
Expand All @@ -40,11 +42,16 @@ async def main():
# # resp = await gizmo.do_one_bidi_stream(["arg1", "arg2", "arg3"])
# # print("do_one_bidi_stream result:", resp)

# # ####### SUMMATION ####### #
# ####### SUMMATION ####### #
summer = SummationService.from_robot(robot, name="mysum1")
sum = await summer.sum([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
print(f"The sum of the numbers [0, 10) is {sum}")

# ####### BASE ####### #
base = Base.from_robot(robot, name="base1")
resp = await base.is_moving()
print(f"The robot's base is{' ' if resp else ' not '}moving.")

await robot.close()


Expand Down
9 changes: 9 additions & 0 deletions examples/complex_module/src/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
This file registers the MyBase model with the Python SDK.
"""

from viam.components.base import Base
from viam.resource.registry import Registry, ResourceCreatorRegistration
from .my_base import MyBase

Registry.register_resource_creator(Base.SUBTYPE, MyBase.MODEL, ResourceCreatorRegistration(MyBase.new, MyBase.validate_config))
145 changes: 145 additions & 0 deletions examples/complex_module/src/base/my_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from typing import Any, ClassVar, Mapping, Dict, List, Optional, cast, Sequence

from typing_extensions import Self

from viam.components.base import Base
from viam.components.motor import Motor
from viam.module.types import Reconfigurable
from viam.proto.app.robot import ComponentConfig
from viam.resource.base import ResourceBase
from viam.proto.common import Geometry, Vector3, ResourceName
from viam.resource.types import Model, ModelFamily
from viam.utils import struct_to_dict


class MyBase(Base, Reconfigurable):
"""
MyBase implements a base that only supports set_power (basic forward/back/turn controls), is_moving (check if in motion), and stop (stop
all motion).

It inherits from the built-in resource subtype Base and conforms to the ``Reconfigurable`` protocol, which signifies that this component
can be reconfigured. Additionally, it specifies a constructor function ``MyBase.new`` which conforms to the
``resource.types.ResourceCreator`` type required for all models. It also specifies a validator function `MyBase.validate_config` which
conforms to the ``resource.types.Validator`` type and returns implicit dependencies for the model.
"""

# Subclass the Viam Base component and implement the required functions
MODEL: ClassVar[Model] = Model(ModelFamily("acme", "demo"), "mybase")

def __init__(self, name: str):
super().__init__(name)

# Constructor
@classmethod
def new(cls, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]) -> Self:
base = cls(config.name)
base.reconfigure(config, dependencies)
return base

# Validates JSON Configuration
@classmethod
def validate_config(cls, config: ComponentConfig) -> Sequence[str]:
attributes_dict = struct_to_dict(config.attributes)
left_name = attributes_dict.get("left", "")
assert isinstance(left_name, str)
if left_name == "":
raise Exception("A left attribute is required for a MyBase component.")

right_name = attributes_dict.get("right", "")
assert isinstance(right_name, str)
if right_name == "":
raise Exception("A right attribute is required for a MyBase component.")
return [left_name, right_name]

# Handles attribute reconfiguration
def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]):
attributes_dict = struct_to_dict(config.attributes)
left_name = attributes_dict.get("left")
right_name = attributes_dict.get("right")

assert isinstance(left_name, str) and isinstance(right_name, str)

left_motor = dependencies[Motor.get_resource_name(left_name)]
right_motor = dependencies[Motor.get_resource_name(right_name)]

self.left = cast(Motor, left_motor)
self.right = cast(Motor, right_motor)

# Not implemented
async def move_straight(
self,
distance: int,
velocity: float,
*,
extra: Optional[Dict[str, Any]] = None,
timeout: Optional[float] = None,
**kwargs,
):
raise NotImplementedError()

# Not implemented
async def spin(
self,
angle: float,
velocity: float,
*,
extra: Optional[Dict[str, Any]] = None,
timeout: Optional[float] = None,
**kwargs,
):
raise NotImplementedError()

# Set the linear and angular velocity of the left and right motors on the base
async def set_power(
self,
linear: Vector3,
angular: Vector3,
*,
extra: Optional[Dict[str, Any]] = None,
timeout: Optional[float] = None,
**kwargs,
):
# stop the base if absolute value of linear and angular velocity is less than 0.01
if abs(linear.y) < 0.01 and abs(angular.z) < 0.01:
await self.stop(extra=extra, timeout=timeout)

# use linear and angular velocity to calculate percentage of max power to pass to SetPower for left & right motors
sum = abs(linear.y) + abs(angular.z)

await self.left.set_power(power=((linear.y - angular.z) / sum), extra=extra, timeout=timeout)
await self.right.set_power(power=((linear.y - angular.z) / sum), extra=extra, timeout=timeout)

# Not implemented
async def set_velocity(
self,
linear: Vector3,
angular: Vector3,
*,
extra: Optional[Dict[str, Any]] = None,
timeout: Optional[float] = None,
**kwargs,
):
raise NotImplementedError()

# Stop the base from moving by stopping both motors
async def stop(
self,
*,
extra: Optional[Dict[str, Any]] = None,
timeout: Optional[float] = None,
**kwargs,
):
await self.left.stop(extra=extra, timeout=timeout)
await self.right.stop(extra=extra, timeout=timeout)

# Check if either motor on the base is moving with motors' is_moving
async def is_moving(self) -> bool:
return await self.left.is_moving() or await self.right.is_moving()

# Not implemented
async def get_properties(self, *, timeout: Optional[float] | None = None, **kwargs) -> Base.Properties:
raise NotImplementedError()

# Not implemented
async def get_geometries(self) -> List[Geometry]:
raise NotImplementedError()
8 changes: 5 additions & 3 deletions examples/complex_module/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@

from viam.module.module import Module
from viam.components.arm import Arm
from viam.components.base import Base

from .arm.my_arm import MyArm
from .gizmo import Gizmo, MyGizmo
from .summation import MySummationService, SummationService
from .base.my_base import MyBase
from .gizmo.my_gizmo import Gizmo, MyGizmo
from .summation.my_summation import SummationService, MySummationService


async def main():
"""This function creates and starts a new module, after adding all desired resource models.
Resource models must be pre-registered. For an example, see the `gizmo.__init__.py` file.
"""

module = Module.from_args()
module.add_model_from_registry(Gizmo.SUBTYPE, MyGizmo.MODEL)
module.add_model_from_registry(SummationService.SUBTYPE, MySummationService.MODEL)
module.add_model_from_registry(Arm.SUBTYPE, MyArm.MODEL)
module.add_model_from_registry(Base.SUBTYPE, MyBase.MODEL)
await module.start()


Expand Down