Skip to content

Commit 8659416

Browse files
author
Bashar
authored
[RSDK 4187] Custom Base Example (#405)
1 parent 4ccdc1b commit 8659416

File tree

5 files changed

+198
-5
lines changed

5 files changed

+198
-5
lines changed

examples/complex_module/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
# VIAM Complex Module Example
2+
23
This example goes through how to create custom modular resources using Viam's python SDK, and how to connect it to a Robot.
34

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

67
## Purpose
8+
79
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.
810

911
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.
1012

1113
## Project structure
14+
1215
The definition of the new resources are in the `src` directory. Within this directory are the `proto`, `gizmo`, `arm`, and `summation` subdirectories.
1316

1417
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.
@@ -19,18 +22,22 @@ Similarly, the `summation` directory contains the analogous definitions for the
1922

2023
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).
2124

25+
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).
26+
2227
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.
2328

2429
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.
2530

2631
## Configuring and using the module
32+
2733
These steps assume that you have a robot available at [app.viam.com](app.viam.com).
2834

2935
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.
3036

3137
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.
3238

3339
An example configuration for an Arm component, a Gizmo component, and a Summation service could look like this:
40+
3441
```json
3542
{
3643
"components": [
@@ -64,6 +71,29 @@ An example configuration for an Arm component, a Gizmo component, and a Summatio
6471
"board": ""
6572
},
6673
"depends_on": []
74+
},
75+
{
76+
"name": "motor2",
77+
"type": "motor",
78+
"model": "fake",
79+
"attributes": {
80+
"pins": {
81+
"dir": "",
82+
"pwm": ""
83+
},
84+
"board": ""
85+
},
86+
"depends_on": []
87+
},
88+
{
89+
"name": "base1",
90+
"type": "base",
91+
"attributes": {
92+
"left": "motor1",
93+
"right": "motor2"
94+
},
95+
"model": "acme:demo:mybase",
96+
"depends_on": []
6797
}
6898
],
6999
"services": [

examples/complex_module/client.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from viam import logging
77
from viam.robot.client import RobotClient
88
from viam.rpc.dial import Credentials, DialOptions
9+
from viam.components.base import Base
910

1011

1112
async def connect():
@@ -18,7 +19,8 @@ async def main():
1819
robot = await connect()
1920

2021
print("Resources:")
21-
print(robot.resource_names)
22+
for resource in robot.resource_names:
23+
print(resource)
2224

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

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

50+
# ####### BASE ####### #
51+
base = Base.from_robot(robot, name="base1")
52+
resp = await base.is_moving()
53+
print(f"The robot's base is{' ' if resp else ' not '}moving.")
54+
4855
await robot.close()
4956

5057

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
This file registers the MyBase model with the Python SDK.
3+
"""
4+
5+
from viam.components.base import Base
6+
from viam.resource.registry import Registry, ResourceCreatorRegistration
7+
from .my_base import MyBase
8+
9+
Registry.register_resource_creator(Base.SUBTYPE, MyBase.MODEL, ResourceCreatorRegistration(MyBase.new, MyBase.validate_config))
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from typing import Any, ClassVar, Mapping, Dict, List, Optional, cast, Sequence
2+
3+
from typing_extensions import Self
4+
5+
from viam.components.base import Base
6+
from viam.components.motor import Motor
7+
from viam.module.types import Reconfigurable
8+
from viam.proto.app.robot import ComponentConfig
9+
from viam.resource.base import ResourceBase
10+
from viam.proto.common import Geometry, Vector3, ResourceName
11+
from viam.resource.types import Model, ModelFamily
12+
from viam.utils import struct_to_dict
13+
14+
15+
class MyBase(Base, Reconfigurable):
16+
"""
17+
MyBase implements a base that only supports set_power (basic forward/back/turn controls), is_moving (check if in motion), and stop (stop
18+
all motion).
19+
20+
It inherits from the built-in resource subtype Base and conforms to the ``Reconfigurable`` protocol, which signifies that this component
21+
can be reconfigured. Additionally, it specifies a constructor function ``MyBase.new`` which conforms to the
22+
``resource.types.ResourceCreator`` type required for all models. It also specifies a validator function `MyBase.validate_config` which
23+
conforms to the ``resource.types.Validator`` type and returns implicit dependencies for the model.
24+
"""
25+
26+
# Subclass the Viam Base component and implement the required functions
27+
MODEL: ClassVar[Model] = Model(ModelFamily("acme", "demo"), "mybase")
28+
29+
def __init__(self, name: str):
30+
super().__init__(name)
31+
32+
# Constructor
33+
@classmethod
34+
def new(cls, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]) -> Self:
35+
base = cls(config.name)
36+
base.reconfigure(config, dependencies)
37+
return base
38+
39+
# Validates JSON Configuration
40+
@classmethod
41+
def validate_config(cls, config: ComponentConfig) -> Sequence[str]:
42+
attributes_dict = struct_to_dict(config.attributes)
43+
left_name = attributes_dict.get("left", "")
44+
assert isinstance(left_name, str)
45+
if left_name == "":
46+
raise Exception("A left attribute is required for a MyBase component.")
47+
48+
right_name = attributes_dict.get("right", "")
49+
assert isinstance(right_name, str)
50+
if right_name == "":
51+
raise Exception("A right attribute is required for a MyBase component.")
52+
return [left_name, right_name]
53+
54+
# Handles attribute reconfiguration
55+
def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]):
56+
attributes_dict = struct_to_dict(config.attributes)
57+
left_name = attributes_dict.get("left")
58+
right_name = attributes_dict.get("right")
59+
60+
assert isinstance(left_name, str) and isinstance(right_name, str)
61+
62+
left_motor = dependencies[Motor.get_resource_name(left_name)]
63+
right_motor = dependencies[Motor.get_resource_name(right_name)]
64+
65+
self.left = cast(Motor, left_motor)
66+
self.right = cast(Motor, right_motor)
67+
68+
# Not implemented
69+
async def move_straight(
70+
self,
71+
distance: int,
72+
velocity: float,
73+
*,
74+
extra: Optional[Dict[str, Any]] = None,
75+
timeout: Optional[float] = None,
76+
**kwargs,
77+
):
78+
raise NotImplementedError()
79+
80+
# Not implemented
81+
async def spin(
82+
self,
83+
angle: float,
84+
velocity: float,
85+
*,
86+
extra: Optional[Dict[str, Any]] = None,
87+
timeout: Optional[float] = None,
88+
**kwargs,
89+
):
90+
raise NotImplementedError()
91+
92+
# Set the linear and angular velocity of the left and right motors on the base
93+
async def set_power(
94+
self,
95+
linear: Vector3,
96+
angular: Vector3,
97+
*,
98+
extra: Optional[Dict[str, Any]] = None,
99+
timeout: Optional[float] = None,
100+
**kwargs,
101+
):
102+
# stop the base if absolute value of linear and angular velocity is less than 0.01
103+
if abs(linear.y) < 0.01 and abs(angular.z) < 0.01:
104+
await self.stop(extra=extra, timeout=timeout)
105+
106+
# use linear and angular velocity to calculate percentage of max power to pass to SetPower for left & right motors
107+
sum = abs(linear.y) + abs(angular.z)
108+
109+
await self.left.set_power(power=((linear.y - angular.z) / sum), extra=extra, timeout=timeout)
110+
await self.right.set_power(power=((linear.y - angular.z) / sum), extra=extra, timeout=timeout)
111+
112+
# Not implemented
113+
async def set_velocity(
114+
self,
115+
linear: Vector3,
116+
angular: Vector3,
117+
*,
118+
extra: Optional[Dict[str, Any]] = None,
119+
timeout: Optional[float] = None,
120+
**kwargs,
121+
):
122+
raise NotImplementedError()
123+
124+
# Stop the base from moving by stopping both motors
125+
async def stop(
126+
self,
127+
*,
128+
extra: Optional[Dict[str, Any]] = None,
129+
timeout: Optional[float] = None,
130+
**kwargs,
131+
):
132+
await self.left.stop(extra=extra, timeout=timeout)
133+
await self.right.stop(extra=extra, timeout=timeout)
134+
135+
# Check if either motor on the base is moving with motors' is_moving
136+
async def is_moving(self) -> bool:
137+
return await self.left.is_moving() or await self.right.is_moving()
138+
139+
# Not implemented
140+
async def get_properties(self, *, timeout: Optional[float] | None = None, **kwargs) -> Base.Properties:
141+
raise NotImplementedError()
142+
143+
# Not implemented
144+
async def get_geometries(self) -> List[Geometry]:
145+
raise NotImplementedError()

examples/complex_module/src/main.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@
22

33
from viam.module.module import Module
44
from viam.components.arm import Arm
5+
from viam.components.base import Base
56

67
from .arm.my_arm import MyArm
7-
from .gizmo import Gizmo, MyGizmo
8-
from .summation import MySummationService, SummationService
8+
from .base.my_base import MyBase
9+
from .gizmo.my_gizmo import Gizmo, MyGizmo
10+
from .summation.my_summation import SummationService, MySummationService
911

1012

1113
async def main():
1214
"""This function creates and starts a new module, after adding all desired resource models.
1315
Resource models must be pre-registered. For an example, see the `gizmo.__init__.py` file.
1416
"""
15-
1617
module = Module.from_args()
1718
module.add_model_from_registry(Gizmo.SUBTYPE, MyGizmo.MODEL)
1819
module.add_model_from_registry(SummationService.SUBTYPE, MySummationService.MODEL)
1920
module.add_model_from_registry(Arm.SUBTYPE, MyArm.MODEL)
21+
module.add_model_from_registry(Base.SUBTYPE, MyBase.MODEL)
2022
await module.start()
2123

2224

0 commit comments

Comments
 (0)