Skip to content

Commit 87741a8

Browse files
APP-7497: Add Button client, server, and fake model
1 parent 1c4d115 commit 87741a8

File tree

11 files changed

+486
-0
lines changed

11 files changed

+486
-0
lines changed

components/button/button.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Package button defines a button on your machine.
2+
package button
3+
4+
import (
5+
"context"
6+
7+
pb "go.viam.com/api/component/button/v1"
8+
9+
"go.viam.com/rdk/resource"
10+
"go.viam.com/rdk/robot"
11+
)
12+
13+
func init() {
14+
resource.RegisterAPI(API, resource.APIRegistration[Button]{
15+
RPCServiceServerConstructor: NewRPCServiceServer,
16+
RPCServiceHandler: pb.RegisterButtonServiceHandlerFromEndpoint,
17+
RPCServiceDesc: &pb.ButtonService_ServiceDesc,
18+
RPCClient: NewClientFromConn,
19+
})
20+
}
21+
22+
// SubtypeName is a constant that identifies the component resource API string.
23+
const SubtypeName = "button"
24+
25+
// API is a variable that identifies the component resource API.
26+
var API = resource.APINamespaceRDK.WithComponentType(SubtypeName)
27+
28+
// Named is a helper for getting the named grippers's typed resource name.
29+
func Named(name string) resource.Name {
30+
return resource.NewName(API, name)
31+
}
32+
33+
// A Button represents a physical button.
34+
type Button interface {
35+
resource.Resource
36+
37+
// Push pushes the button.
38+
// This will block until done or a new operation cancels this one.
39+
Push(ctx context.Context, extra map[string]interface{}) error
40+
}
41+
42+
// FromRobot is a helper for getting the named Button from the given Robot.
43+
func FromRobot(r robot.Robot, name string) (Button, error) {
44+
return robot.ResourceFromRobot[Button](r, Named(name))
45+
}
46+
47+
// NamesFromRobot is a helper for getting all gripper names from the given Robot.
48+
func NamesFromRobot(r robot.Robot) []string {
49+
return robot.NamesByAPI(r, API)
50+
}

components/button/button_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package button_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"go.viam.com/test"
8+
9+
"go.viam.com/rdk/components/button"
10+
"go.viam.com/rdk/components/button/fake"
11+
"go.viam.com/rdk/resource"
12+
)
13+
14+
const (
15+
testButtonName = "button1"
16+
testButtonName2 = "button2"
17+
failButtonName = "button3"
18+
missingButtonName = "button4"
19+
)
20+
21+
func TestGetGeometries(t *testing.T) {
22+
cfg := resource.Config{
23+
Name: "fakeButton",
24+
API: button.API,
25+
}
26+
button, err := fake.NewButton(context.Background(), nil, cfg, nil)
27+
test.That(t, err, test.ShouldBeNil)
28+
29+
err = button.Push(context.Background(), nil)
30+
test.That(t, err, test.ShouldBeNil)
31+
}

components/button/client.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Package button contains a gRPC based button client.
2+
package button
3+
4+
import (
5+
"context"
6+
7+
pb "go.viam.com/api/component/button/v1"
8+
"go.viam.com/utils/protoutils"
9+
"go.viam.com/utils/rpc"
10+
11+
"go.viam.com/rdk/logging"
12+
rprotoutils "go.viam.com/rdk/protoutils"
13+
"go.viam.com/rdk/resource"
14+
)
15+
16+
// client implements GripperServiceClient.
17+
type client struct {
18+
resource.Named
19+
resource.TriviallyReconfigurable
20+
resource.TriviallyCloseable
21+
name string
22+
client pb.ButtonServiceClient
23+
logger logging.Logger
24+
}
25+
26+
// NewClientFromConn constructs a new Client from connection passed in.
27+
func NewClientFromConn(
28+
ctx context.Context,
29+
conn rpc.ClientConn,
30+
remoteName string,
31+
name resource.Name,
32+
logger logging.Logger,
33+
) (Button, error) {
34+
c := pb.NewButtonServiceClient(conn)
35+
return &client{
36+
Named: name.PrependRemote(remoteName).AsNamed(),
37+
name: name.ShortName(),
38+
client: c,
39+
logger: logger,
40+
}, nil
41+
}
42+
43+
func (c *client) Push(ctx context.Context, extra map[string]interface{}) error {
44+
ext, err := protoutils.StructToStructPb(extra)
45+
if err != nil {
46+
return err
47+
}
48+
_, err = c.client.Push(ctx, &pb.PushRequest{
49+
Name: c.name,
50+
Extra: ext,
51+
})
52+
return err
53+
}
54+
55+
func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) {
56+
return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd)
57+
}

components/button/client_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package button_test
2+
3+
import (
4+
"context"
5+
"net"
6+
"testing"
7+
8+
"go.viam.com/test"
9+
"go.viam.com/utils/rpc"
10+
11+
"go.viam.com/rdk/components/button"
12+
viamgrpc "go.viam.com/rdk/grpc"
13+
"go.viam.com/rdk/logging"
14+
"go.viam.com/rdk/resource"
15+
"go.viam.com/rdk/testutils"
16+
"go.viam.com/rdk/testutils/inject"
17+
)
18+
19+
func TestClient(t *testing.T) {
20+
logger := logging.NewTestLogger(t)
21+
listener1, err := net.Listen("tcp", "localhost:0")
22+
test.That(t, err, test.ShouldBeNil)
23+
rpcServer, err := rpc.NewServer(logger, rpc.WithUnauthenticated())
24+
test.That(t, err, test.ShouldBeNil)
25+
26+
var buttonPushed string
27+
var extraOptions map[string]interface{}
28+
29+
injectButton := &inject.Button{}
30+
injectButton.PushFunc = func(ctx context.Context, extra map[string]interface{}) error {
31+
extraOptions = extra
32+
buttonPushed = testButtonName
33+
return nil
34+
}
35+
36+
injectButton2 := &inject.Button{}
37+
injectButton2.PushFunc = func(ctx context.Context, extra map[string]interface{}) error {
38+
buttonPushed = failButtonName
39+
return errCantPush
40+
}
41+
42+
buttonSvc, err := resource.NewAPIResourceCollection(
43+
button.API,
44+
map[resource.Name]button.Button{button.Named(testButtonName): injectButton, button.Named(failButtonName): injectButton2})
45+
test.That(t, err, test.ShouldBeNil)
46+
resourceAPI, ok, err := resource.LookupAPIRegistration[button.Button](button.API)
47+
test.That(t, err, test.ShouldBeNil)
48+
test.That(t, ok, test.ShouldBeTrue)
49+
test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, buttonSvc), test.ShouldBeNil)
50+
51+
injectButton.DoFunc = testutils.EchoFunc
52+
53+
go rpcServer.Serve(listener1)
54+
defer rpcServer.Stop()
55+
56+
// failing
57+
t.Run("Failing client", func(t *testing.T) {
58+
cancelCtx, cancel := context.WithCancel(context.Background())
59+
cancel()
60+
_, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger)
61+
test.That(t, err, test.ShouldNotBeNil)
62+
test.That(t, err, test.ShouldBeError, context.Canceled)
63+
})
64+
65+
// working
66+
t.Run("button client 1", func(t *testing.T) {
67+
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
68+
test.That(t, err, test.ShouldBeNil)
69+
button1Client, err := button.NewClientFromConn(context.Background(), conn, "", button.Named(testButtonName), logger)
70+
test.That(t, err, test.ShouldBeNil)
71+
72+
// DoCommand
73+
resp, err := button1Client.DoCommand(context.Background(), testutils.TestCommand)
74+
test.That(t, err, test.ShouldBeNil)
75+
test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"])
76+
test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"])
77+
78+
extra := map[string]interface{}{"foo": "Push"}
79+
err = button1Client.Push(context.Background(), extra)
80+
test.That(t, err, test.ShouldBeNil)
81+
test.That(t, extraOptions, test.ShouldResemble, extra)
82+
test.That(t, buttonPushed, test.ShouldEqual, testButtonName)
83+
84+
test.That(t, button1Client.Close(context.Background()), test.ShouldBeNil)
85+
test.That(t, conn.Close(), test.ShouldBeNil)
86+
})
87+
88+
t.Run("button client 2", func(t *testing.T) {
89+
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
90+
test.That(t, err, test.ShouldBeNil)
91+
client2, err := resourceAPI.RPCClient(context.Background(), conn, "", button.Named(failButtonName), logger)
92+
test.That(t, err, test.ShouldBeNil)
93+
94+
extra := map[string]interface{}{}
95+
err = client2.Push(context.Background(), extra)
96+
test.That(t, err, test.ShouldNotBeNil)
97+
test.That(t, err.Error(), test.ShouldContainSubstring, errCantPush.Error())
98+
test.That(t, buttonPushed, test.ShouldEqual, failButtonName)
99+
100+
test.That(t, client2.Close(context.Background()), test.ShouldBeNil)
101+
test.That(t, conn.Close(), test.ShouldBeNil)
102+
})
103+
}

components/button/fake/button.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Package fake implements a fake button.
2+
package fake
3+
4+
import (
5+
"context"
6+
"sync"
7+
8+
"go.viam.com/rdk/components/button"
9+
"go.viam.com/rdk/logging"
10+
"go.viam.com/rdk/resource"
11+
)
12+
13+
var model = resource.DefaultModelFamily.WithModel("fake")
14+
15+
// Config is the config for a fake button.
16+
type Config struct {
17+
resource.TriviallyValidateConfig
18+
}
19+
20+
func init() {
21+
resource.RegisterComponent(button.API, model, resource.Registration[button.Button, *Config]{Constructor: NewButton})
22+
}
23+
24+
// Button is a fake button that logs when it is pressed
25+
type Button struct {
26+
resource.Named
27+
resource.TriviallyCloseable
28+
mu sync.Mutex
29+
logger logging.Logger
30+
}
31+
32+
// NewButton instantiates a new button of the fake model type.
33+
func NewButton(
34+
ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger,
35+
) (button.Button, error) {
36+
b := &Button{
37+
Named: conf.ResourceName().AsNamed(),
38+
logger: logger,
39+
}
40+
if err := b.Reconfigure(ctx, deps, conf); err != nil {
41+
return nil, err
42+
}
43+
return b, nil
44+
}
45+
46+
// Reconfigure reconfigures the button atomically and in place.
47+
func (b *Button) Reconfigure(_ context.Context, _ resource.Dependencies, conf resource.Config) error {
48+
b.mu.Lock()
49+
defer b.mu.Unlock()
50+
51+
return nil
52+
}
53+
54+
// Push logs the push
55+
func (b *Button) Push(ctx context.Context, extra map[string]interface{}) error {
56+
b.logger.Info("pushed button")
57+
return nil
58+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Package register registers all relevant buttons and also API specific functions
2+
package register
3+
4+
import (
5+
// for buttons.
6+
_ "go.viam.com/rdk/components/button/fake"
7+
)

components/button/server.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Package button contains a gRPC based button service server.
2+
package button
3+
4+
import (
5+
"context"
6+
7+
commonpb "go.viam.com/api/common/v1"
8+
pb "go.viam.com/api/component/button/v1"
9+
10+
"go.viam.com/rdk/protoutils"
11+
"go.viam.com/rdk/resource"
12+
)
13+
14+
// serviceServer implements the ButtonService from button.proto.
15+
type serviceServer struct {
16+
pb.UnimplementedButtonServiceServer
17+
coll resource.APIResourceCollection[Button]
18+
}
19+
20+
// NewRPCServiceServer constructs an gripper gRPC service server.
21+
// It is intentionally untyped to prevent use outside of tests.
22+
func NewRPCServiceServer(coll resource.APIResourceCollection[Button]) interface{} {
23+
return &serviceServer{coll: coll}
24+
}
25+
26+
// Pushes a button
27+
func (s *serviceServer) Push(ctx context.Context, req *pb.PushRequest) (*pb.PushResponse, error) {
28+
button, err := s.coll.Resource(req.Name)
29+
if err != nil {
30+
return nil, err
31+
}
32+
return &pb.PushResponse{}, button.Push(ctx, req.Extra.AsMap())
33+
}
34+
35+
// DoCommand receives arbitrary commands.
36+
func (s *serviceServer) DoCommand(ctx context.Context,
37+
req *commonpb.DoCommandRequest,
38+
) (*commonpb.DoCommandResponse, error) {
39+
gripper, err := s.coll.Resource(req.GetName())
40+
if err != nil {
41+
return nil, err
42+
}
43+
return protoutils.DoFromResourceServer(ctx, gripper, req)
44+
}

0 commit comments

Comments
 (0)