Skip to content

Commit 1c481d3

Browse files
authored
[RSDK-2227] Modular Services (#237)
1 parent 9073501 commit 1c481d3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+1496
-761
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ clean:
22
find . -type d -name '__pycache__' | xargs rm -rf
33

44
_lint:
5-
flake8 --exclude=**/gen/**,*_grpc.py,*_pb2.py,*_pb2.pyi
5+
flake8 --exclude=**/gen/**,*_grpc.py,*_pb2.py,*_pb2.pyi,.tox
66

77
lint:
88
poetry run make _lint

examples/module/README

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ Modular resources allows you to define custom components and services, and add t
99
For more information, see the [documentation](https://docs.viam.com/program/extend/modular-resources/).
1010

1111
## Project structure
12-
The definition of the new resources are in the `src` directory. Within this directory are the `proto` and `gizmo` subdirectories.
12+
The definition of the new resources are in the `src` directory. Within this directory are the `proto`, `gizmo`, and `summation` subdirectories.
1313

14-
The `proto` directory contains the `gizmo.proto` definition of all the message types and calls that can be made to the component. It also has the compiled python output of the protobuf definition.
14+
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.
1515

1616
The `gizmo` directory contains all the necessary definitions for creating a custom `Gizmo` component type. The `api.py` file defines what a `Gizmo` can do (mirroring the `proto` definition), implements the gRPC `GizmoService` for receiving calls, and the gRPC `GizmoClient` for making calls. See the [API docs](https://docs.viam.com/program/extend/modular-resources/#apis) for more info. The `my_gizmo.py` file in contains the unique implementation of a `Gizmo`. This is defined as a specific `Model`. See the [Model docs](https://docs.viam.com/program/extend/modular-resources/#models) for more info.
1717

18+
Similarly, the `summation` directory contains the analogous definitions for the `Summation` service type. The files in this directory mirror the files in the `gizmo` directory.
19+
1820
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.
1921

2022
Outside the `src` directory, there is the `client.py` file. This can be used to test the module once it's connected to the robot. You will have to update the credentials and robot address in that file.
@@ -24,9 +26,9 @@ These steps assume that you have a robot available at [app.viam.com](app.viam.co
2426

2527
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, this could be `/home/viam-python-sdk/examples/module/run.sh`. See the [documentation](https://docs.viam.com/program/extend/modular-resources/#use-a-modular-resource-with-your-robot) for more details.
2628

27-
Once the module has been added to your robot, you will then need to add a 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.
29+
Once the module has been added to your robot, you will then need to add a 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 add a service in a similar manner.
2830

29-
An example configuration for a Gizmo could look like this:
31+
An example configuration for a Gizmo component and a Summation service could look like this:
3032
```json
3133
{
3234
"components": [
@@ -41,11 +43,24 @@ An example configuration for a Gizmo could look like this:
4143
"depends_on": []
4244
}
4345
],
46+
"services": [
47+
{
48+
"name": "mysum1",
49+
"type": "summation",
50+
"namespace": "acme",
51+
"model": "acme:demo:mysum",
52+
"attributes": {
53+
"subtract": false
54+
}
55+
}
56+
],
4457
"modules": [
4558
{
46-
"name": "gizmo-module",
59+
"name": "my-module",
4760
"executable_path": "/home/viam-python-sdk/examples/module/run.sh"
4861
}
4962
]
5063
}
5164
```
65+
66+
After the robot has started and connected to the module, you can use the provided `client.py` to connect to your robot and make calls to your custom, modular resources.Ø

examples/module/client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22

33
from src.gizmo import Gizmo
4+
from src.summation import SummationService
45

56
from viam import logging
67
from viam.robot.client import RobotClient
@@ -19,6 +20,7 @@ async def main():
1920
print("Resources:")
2021
print(robot.resource_names)
2122

23+
# ####### GIZMO ####### #
2224
gizmo = Gizmo.from_robot(robot, name="gizmo1")
2325
resp = await gizmo.do_one("arg1")
2426
print("do_one result:", resp)
@@ -38,6 +40,11 @@ async def main():
3840
# resp = await gizmo.do_one_bidi_stream(["arg1", "arg2", "arg3"])
3941
# print("do_one_bidi_stream result:", resp)
4042

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

4350

examples/module/src/gizmo/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99

1010
Registry.register_subtype(ResourceRegistration(Gizmo, GizmoService, lambda name, channel: GizmoClient(name, channel)))
1111

12-
Registry.register_component_model(Gizmo.SUBTYPE, MyGizmo.MODEL, MyGizmo.new)
12+
Registry.register_resource_creator(Gizmo.SUBTYPE, MyGizmo.MODEL, MyGizmo.new)

examples/module/src/gizmo/api.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,32 @@
55
the gRPC service that will handle calls to the component,
66
and the gRPC client that will be able to make calls to this component.
77
8-
In this example, the ```Gizmo``` abstract class defines what functionality is required for all Gizmos. It extends ```BaseComponent```,
9-
as all component types must. It also defines its specific ```SUBTYPE```, which is used internally to keep track of supported types.
8+
In this example, the ``Gizmo`` abstract class defines what functionality is required for all Gizmos. It extends ``ComponentBase``,
9+
as all component types must. It also defines its specific ``SUBTYPE``, which is used internally to keep track of supported types.
1010
11-
The ```GizmoService``` implements the gRPC service for the Gizmo. This will allow other robots and clients to make requests of the Gizmo.
12-
It extends both from ```GizmoServiceBase``` and ```ComponentServiceBase[Gizmo]```. The former is the gRPC service as defined by the proto,
11+
The ``GizmoService`` implements the gRPC service for the Gizmo. This will allow other robots and clients to make requests of the Gizmo.
12+
It extends both from ``GizmoServiceBase`` and ``ResourceRPCServiceBase[Gizmo]``. The former is the gRPC service as defined by the proto,
1313
and the latter is the class that all gRPC services for components must inherit from.
1414
15-
Finally, the ```GizmoClient``` is the gRPC client for a Gizmo. It inherits from Gizmo since it implements all the same functions. The
15+
Finally, the ``GizmoClient`` is the gRPC client for a Gizmo. It inherits from Gizmo since it implements all the same functions. The
1616
implementations are simply gRPC calls to some remote Gizmo.
1717
1818
To see how this custom modular component is registered, see the __init__.py file.
1919
To see the custom implementation of this component, see the my_gizmo.py file.
2020
"""
2121

2222
import abc
23-
from typing import Any, Dict, Final, List, Optional, Sequence
23+
from typing import Final, List, Mapping, Optional, Sequence
2424

2525
from grpclib.client import Channel
2626
from grpclib.server import Stream
2727

2828
from viam.components.component_base import ComponentBase
2929
from viam.components.generic.client import do_command
30-
from viam.components.service_base import ComponentServiceBase
3130
from viam.errors import ResourceNotFoundError
31+
from viam.resource.rpc_service_base import ResourceRPCServiceBase
3232
from viam.resource.types import RESOURCE_TYPE_COMPONENT, Subtype
33+
from viam.utils import ValueTypes
3334

3435
from ..proto.gizmo_grpc import GizmoServiceBase, GizmoServiceStub
3536
from ..proto.gizmo_pb2 import (
@@ -72,7 +73,7 @@ async def do_two(self, arg1: bool, **kwargs) -> str:
7273
...
7374

7475

75-
class GizmoService(GizmoServiceBase, ComponentServiceBase[Gizmo]):
76+
class GizmoService(GizmoServiceBase, ResourceRPCServiceBase[Gizmo]):
7677
"""Example gRPC service for the Gizmo component"""
7778

7879
RESOURCE_TYPE = Gizmo
@@ -82,7 +83,7 @@ async def DoOne(self, stream: Stream[DoOneRequest, DoOneResponse]) -> None:
8283
assert request is not None
8384
name = request.name
8485
try:
85-
gizmo = self.get_component(name)
86+
gizmo = self.get_resource(name)
8687
except ResourceNotFoundError as e:
8788
raise e.grpc_error
8889
resp = await gizmo.do_one(request.arg1)
@@ -97,7 +98,7 @@ async def DoOneClientStream(self, stream: Stream[DoOneClientStreamRequest, DoOne
9798
raise Exception("Unexpectedly received requests for multiple Gizmos")
9899
name = names[0]
99100
try:
100-
gizmo = self.get_component(name)
101+
gizmo = self.get_resource(name)
101102
except ResourceNotFoundError as e:
102103
raise e.grpc_error
103104
resp = await gizmo.do_one_client_stream(args)
@@ -109,7 +110,7 @@ async def DoOneServerStream(self, stream: Stream[DoOneServerStreamRequest, DoOne
109110
assert request is not None
110111
name = request.name
111112
try:
112-
gizmo = self.get_component(name)
113+
gizmo = self.get_resource(name)
113114
except ResourceNotFoundError as e:
114115
raise e.grpc_error
115116
resps = await gizmo.do_one_server_stream(request.arg1)
@@ -127,7 +128,7 @@ async def DoOneBiDiStream(self, stream: Stream[DoOneBiDiStreamRequest, DoOneBiDi
127128
if name != request.name:
128129
raise Exception("Unexpectedly received requests for multiple Gizmos")
129130
try:
130-
gizmo = self.get_component(name)
131+
gizmo = self.get_resource(name)
131132
except ResourceNotFoundError as e:
132133
raise e.grpc_error
133134

@@ -140,7 +141,7 @@ async def DoTwo(self, stream: Stream[DoTwoRequest, DoTwoResponse]) -> None:
140141
assert request is not None
141142
name = request.name
142143
try:
143-
gizmo = self.get_component(name)
144+
gizmo = self.get_resource(name)
144145
except ResourceNotFoundError as e:
145146
raise e.grpc_error
146147
resp = await gizmo.do_two(request.arg1)
@@ -191,8 +192,8 @@ async def do_two(self, arg1: bool) -> str:
191192

192193
async def do_command(
193194
self,
194-
command: Dict[str, Any],
195+
command: Mapping[str, ValueTypes],
195196
*,
196197
timeout: Optional[float] = None,
197-
) -> Dict[str, Any]:
198+
) -> Mapping[str, ValueTypes]:
198199
return await do_command(self.channel, self.name, command, timeout=timeout)

examples/module/src/gizmo/my_gizmo.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,28 @@
22

33
from typing_extensions import Self
44

5-
from viam.components.component_base import ComponentBase
65
from viam.module.types import Reconfigurable
76
from viam.proto.app.robot import ComponentConfig
87
from viam.proto.common import ResourceName
8+
from viam.resource.base import ResourceBase
99
from viam.resource.types import Model, ModelFamily
1010

1111
from ..gizmo.api import Gizmo
1212

1313

1414
class MyGizmo(Gizmo, Reconfigurable):
15-
"""This is the specific implementation of a ```Gizmo``` (defined in api.py).
15+
"""This is the specific implementation of a ``Gizmo`` (defined in api.py).
1616
17-
It inherits from Gizmo, as well conforms to the ```Reconfigurable``` protocol, which signifies that this component can be reconfigured.
18-
It also specifies a function ```MyGizmo.new```, which conforms to the ```resource.types.ComponentCreator``` type, which is required
17+
It inherits from Gizmo, as well conforms to the ``Reconfigurable`` protocol, which signifies that this component can be reconfigured.
18+
It also specifies a function ``MyGizmo.new``, which conforms to the ``resource.ComponentCreator`` type, which is required
1919
for all models.
2020
"""
2121

2222
MODEL: ClassVar[Model] = Model(ModelFamily("acme", "demo"), "mygizmo")
2323
my_arg: str
2424

2525
@classmethod
26-
def new(cls, dependencies: Mapping[ResourceName, ComponentBase], config: ComponentConfig) -> Self:
26+
def new(cls, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]) -> Self:
2727
gizmo = cls(config.name)
2828
gizmo.my_arg = config.attributes.fields["arg1"].string_value
2929
return gizmo
@@ -51,5 +51,5 @@ async def do_one_bidi_stream(self, arg1: Sequence[str], **kwargs) -> Sequence[bo
5151
async def do_two(self, arg1: bool, **kwargs) -> str:
5252
return f"arg1={arg1}"
5353

54-
def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceName, ComponentBase]):
54+
def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]):
5555
self.my_arg = config.attributes.fields["arg1"].string_value

examples/module/src/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from viam.module.module import Module
55

66
from .gizmo import Gizmo, MyGizmo
7+
from .summation import MySummationService, SummationService
78

89

910
async def main(address: str):
@@ -16,6 +17,7 @@ async def main(address: str):
1617

1718
module = Module(address)
1819
module.add_model_from_registry(Gizmo.SUBTYPE, MyGizmo.MODEL)
20+
module.add_model_from_registry(SummationService.SUBTYPE, MySummationService.MODEL)
1921
await module.start()
2022

2123

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
syntax = "proto3";
2+
3+
package acme.service.summation.v1;
4+
5+
import "google/api/annotations.proto";
6+
7+
service SummationService {
8+
rpc Sum(SumRequest) returns (SumResponse) {
9+
option (google.api.http) = {
10+
post: "/acme/api/v1/service/summation/{name}/sum"
11+
};
12+
}
13+
}
14+
15+
message SumRequest {
16+
string name = 1;
17+
repeated double numbers = 2;
18+
}
19+
20+
message SumResponse {
21+
double sum = 1;
22+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Generated by the Protocol Buffers compiler. DO NOT EDIT!
2+
# source: src/proto/summation.proto
3+
# plugin: grpclib.plugin.main
4+
import abc
5+
import typing
6+
7+
import grpclib.const
8+
import grpclib.client
9+
10+
if typing.TYPE_CHECKING:
11+
import grpclib.server
12+
13+
import google.api.annotations_pb2
14+
from .. import proto
15+
16+
17+
class SummationServiceBase(abc.ABC):
18+
@abc.abstractmethod
19+
async def Sum(self, stream: "grpclib.server.Stream[proto.summation_pb2.SumRequest, proto.summation_pb2.SumResponse]") -> None:
20+
pass
21+
22+
def __mapping__(self) -> typing.Dict[str, grpclib.const.Handler]:
23+
return {
24+
"/acme.service.summation.v1.SummationService/Sum": grpclib.const.Handler(
25+
self.Sum,
26+
grpclib.const.Cardinality.UNARY_UNARY,
27+
proto.summation_pb2.SumRequest,
28+
proto.summation_pb2.SumResponse,
29+
),
30+
}
31+
32+
33+
class SummationServiceStub:
34+
def __init__(self, channel: grpclib.client.Channel) -> None:
35+
self.Sum = grpclib.client.UnaryUnaryMethod(
36+
channel,
37+
"/acme.service.summation.v1.SummationService/Sum",
38+
proto.summation_pb2.SumRequest,
39+
proto.summation_pb2.SumResponse,
40+
)

examples/module/src/proto/summation_pb2.py

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)