Skip to content

Commit dee3547

Browse files
authored
RSDK-10279: Button component (#877)
1 parent 0a7aebc commit dee3547

File tree

6 files changed

+254
-0
lines changed

6 files changed

+254
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import viam.gen.component.button.v1.button_pb2
2+
from viam.resource.registry import Registry, ResourceRegistration
3+
4+
from .client import ButtonClient
5+
from .service import ButtonRPCService
6+
from .button import Button
7+
8+
__all__ = ["Button"]
9+
10+
Registry.register_api(ResourceRegistration(Button, ButtonRPCService, lambda name, channel: ButtonClient(name, channel)))
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import abc
2+
from typing import Any, Final, Mapping, Optional
3+
4+
from viam.components.component_base import ComponentBase
5+
from viam.resource.types import API, RESOURCE_NAMESPACE_RDK, RESOURCE_TYPE_COMPONENT
6+
7+
8+
class Button(ComponentBase):
9+
"""
10+
Button represents a device that can be pushed.
11+
12+
This acts as an abstract base class for any drivers representing specific
13+
button implementations. This cannot be used on its own. If the ``__init__()`` function is
14+
overridden, it must call the ``super().__init__()`` function.
15+
16+
::
17+
18+
from viam.components.button import Button
19+
20+
For more information, see `Button component <https://docs.viam.com/dev/reference/apis/components/button/>` _.
21+
"""
22+
23+
API: Final = API( # pyright: ignore [reportIncompatibleVariableOverride]
24+
RESOURCE_NAMESPACE_RDK, RESOURCE_TYPE_COMPONENT, "button"
25+
)
26+
27+
@abc.abstractmethod
28+
async def push(self, *, extra: Optional[Mapping[str, Any]] = None, timeout: Optional[float] = None, **kwargs) -> None:
29+
"""
30+
Push the button.
31+
32+
::
33+
34+
my_button = Button.from_robot(robot=machine, name="my_button")
35+
36+
# Push the button
37+
await my_button.push()
38+
39+
For more information, see `Button component <https://docs.viam.com/dev/reference/apis/components/button/>` _.
40+
"""
41+
...
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from typing import Any, Mapping, Optional
2+
3+
from grpclib.client import Channel
4+
5+
from viam.gen.component.button.v1.button_pb2 import (
6+
PushRequest
7+
)
8+
from viam.proto.common import (
9+
DoCommandRequest,
10+
DoCommandResponse,
11+
)
12+
from viam.proto.component.button import ButtonServiceStub
13+
from viam.resource.rpc_client_base import ReconfigurableResourceRPCClientBase
14+
from viam.utils import (
15+
ValueTypes,
16+
dict_to_struct,
17+
struct_to_dict,
18+
)
19+
20+
from .button import Button
21+
22+
23+
class ButtonClient(Button, ReconfigurableResourceRPCClientBase):
24+
"""
25+
gRPC client for Button component
26+
"""
27+
28+
def __init__(self, name: str, channel: Channel):
29+
self.channel = channel
30+
self.client = ButtonServiceStub(channel)
31+
super().__init__(name)
32+
33+
async def push(
34+
self,
35+
*,
36+
extra: Optional[Mapping[str, Any]] = None,
37+
timeout: Optional[float] = None,
38+
**kwargs,
39+
) -> None:
40+
md = kwargs.get("metadata", self.Metadata()).proto
41+
request = PushRequest(name=self.name, extra=dict_to_struct(extra))
42+
await self.client.Push(request, timeout=timeout, metadata=md)
43+
44+
async def do_command(
45+
self,
46+
command: Mapping[str, ValueTypes],
47+
*,
48+
timeout: Optional[float] = None,
49+
**kwargs,
50+
) -> Mapping[str, ValueTypes]:
51+
md = kwargs.get("metadata", self.Metadata()).proto
52+
request = DoCommandRequest(name=self.name, command=dict_to_struct(command))
53+
response: DoCommandResponse = await self.client.DoCommand(request, timeout=timeout, metadata=md)
54+
return struct_to_dict(response.result)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from grpclib.server import Stream
2+
3+
from viam.gen.component.button.v1.button_pb2 import (
4+
PushRequest,
5+
PushResponse,
6+
)
7+
from viam.proto.common import (
8+
DoCommandRequest,
9+
DoCommandResponse,
10+
)
11+
from viam.proto.component.button import ButtonServiceBase
12+
from viam.resource.rpc_service_base import ResourceRPCServiceBase
13+
from viam.utils import dict_to_struct, struct_to_dict
14+
15+
from .button import Button
16+
17+
18+
class ButtonRPCService(ButtonServiceBase, ResourceRPCServiceBase[Button]):
19+
"""
20+
gRPC Service for a generic Button
21+
"""
22+
23+
RESOURCE_TYPE = Button
24+
25+
async def Push(self, stream: Stream[PushRequest, PushResponse]) -> None:
26+
request = await stream.recv_message()
27+
assert request is not None
28+
name = request.name
29+
button = self.get_resource(name)
30+
timeout = stream.deadline.time_remaining() if stream.deadline else None
31+
await button.push(extra=struct_to_dict(request.extra), timeout=timeout, metadata=stream.metadata)
32+
await stream.send_message(PushResponse())
33+
34+
async def DoCommand(self, stream: Stream[DoCommandRequest, DoCommandResponse]) -> None:
35+
request = await stream.recv_message()
36+
assert request is not None
37+
name = request.name
38+
button = self.get_resource(name)
39+
timeout = stream.deadline.time_remaining() if stream.deadline else None
40+
result = await button.do_command(
41+
command=struct_to_dict(request.command),
42+
timeout=timeout,
43+
metadata=stream.metadata,
44+
)
45+
response = DoCommandResponse(result=dict_to_struct(result))
46+
await stream.send_message(response)

tests/mocks/components.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from viam.components.arm import Arm, JointPositions, KinematicsFileFormat
1616
from viam.components.audio_input import AudioInput
1717
from viam.components.base import Base
18+
from viam.components.button import Button
1819
from viam.components.board import Board, Tick
1920
from viam.components.camera import Camera, DistortionParameters, IntrinsicParameters
2021
from viam.components.encoder import Encoder
@@ -1043,3 +1044,18 @@ async def set_position(
10431044

10441045
async def do_command(self, command: Mapping[str, ValueTypes], *, timeout: Optional[float] = None, **kwargs) -> Mapping[str, ValueTypes]:
10451046
return {"command": command}
1047+
1048+
class MockButton(Button):
1049+
def __init__(self, name: str):
1050+
self.pushed = False
1051+
self.timeout: Optional[float] = None
1052+
self.extra: Optional[Mapping[str, Any]] = None
1053+
super().__init__(name)
1054+
1055+
async def push(self, *, extra: Optional[Mapping[str, Any]] = None, timeout: Optional[float] = None, **kwargs) -> None:
1056+
self.extra = extra
1057+
self.timeout = timeout
1058+
self.pushed = True
1059+
1060+
async def do_command(self, command: Mapping[str, ValueTypes], *, timeout: Optional[float] = None, **kwargs) -> Mapping[str, ValueTypes]:
1061+
return {"command": command}

tests/test_button.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import pytest
2+
from grpclib.testing import ChannelFor
3+
4+
from viam.components.button import ButtonClient
5+
from viam.components.button.service import ButtonRPCService
6+
from viam.gen.component.button.v1.button_pb2 import (
7+
PushRequest
8+
)
9+
from viam.proto.common import (
10+
DoCommandRequest,
11+
DoCommandResponse,
12+
)
13+
from viam.proto.component.button import ButtonServiceStub
14+
from viam.resource.manager import ResourceManager
15+
from viam.utils import dict_to_struct, struct_to_dict
16+
17+
from . import loose_approx
18+
from .mocks.components import MockButton
19+
20+
EXTRA_PARAMS = {"foo": "bar", "baz": [1, 2, 3]}
21+
22+
23+
@pytest.fixture(scope="function")
24+
def button() -> MockButton:
25+
return MockButton(name="button")
26+
27+
28+
class TestButton:
29+
async def test_push(self, button):
30+
await button.push(timeout=1.23, extra=EXTRA_PARAMS)
31+
assert button.pushed is True
32+
assert button.timeout == loose_approx(1.23)
33+
assert button.extra == EXTRA_PARAMS
34+
35+
async def test_do(self, button):
36+
command = {"command": "args"}
37+
resp = await button.do_command(command)
38+
assert resp == {"command": command}
39+
40+
41+
@pytest.fixture(scope="function")
42+
def manager(button) -> ResourceManager:
43+
return ResourceManager([button])
44+
45+
46+
@pytest.fixture(scope="function")
47+
def service(manager) -> ButtonRPCService:
48+
return ButtonRPCService(manager)
49+
50+
51+
class TestService:
52+
async def test_push(self, button, service):
53+
async with ChannelFor([service]) as channel:
54+
client = ButtonServiceStub(channel)
55+
request = PushRequest(name=button.name, extra=dict_to_struct(EXTRA_PARAMS))
56+
assert button.extra is None
57+
await client.Push(request, timeout=1.23)
58+
assert button.pushed is True
59+
assert button.extra == EXTRA_PARAMS
60+
assert button.timeout == loose_approx(1.23)
61+
62+
async def test_do(self, button: MockButton, service: ButtonRPCService):
63+
async with ChannelFor([service]) as channel:
64+
client = ButtonServiceStub(channel)
65+
command = {"command": "args"}
66+
request = DoCommandRequest(name=button.name, command=dict_to_struct(command))
67+
response: DoCommandResponse = await client.DoCommand(request)
68+
result = struct_to_dict(response.result)
69+
assert result == {"command": command}
70+
71+
72+
class TestClient:
73+
async def test_push(self, button, service):
74+
async with ChannelFor([service]) as channel:
75+
client = ButtonClient(button.name, channel)
76+
assert button.extra is None
77+
await client.push(timeout=3.45, extra=EXTRA_PARAMS)
78+
assert button.pushed is True
79+
assert button.extra == EXTRA_PARAMS
80+
assert button.timeout == loose_approx(3.45)
81+
82+
async def test_do(self, button, manager, service):
83+
async with ChannelFor([service]) as channel:
84+
client = ButtonClient(button.name, channel)
85+
command = {"command": "args"}
86+
resp = await client.do_command(command)
87+
assert resp == {"command": command}

0 commit comments

Comments
 (0)