diff --git a/lib/src/resource/base.dart b/lib/src/resource/base.dart index afe2d6acdeb..3a17ef25b9f 100644 --- a/lib/src/resource/base.dart +++ b/lib/src/resource/base.dart @@ -55,6 +55,7 @@ class Subtype { abstract class Resource { abstract String name; + /// Send/Receive arbitrary commands to the [Resource] Future> doCommand(Map command) { throw UnimplementedError(); } diff --git a/lib/src/services/vision.dart b/lib/src/services/vision.dart new file mode 100644 index 00000000000..d2604edbbe5 --- /dev/null +++ b/lib/src/services/vision.dart @@ -0,0 +1,79 @@ +import 'package:fixnum/fixnum.dart'; +import 'package:grpc/grpc_connection_interface.dart'; + +import '../../protos/common/common.dart'; +import '../../protos/service/vision.dart'; +import '../media/image.dart'; +import '../resource/base.dart'; +import '../utils.dart'; + +class VisionClient implements ResourceRPCClient { + final String name; + + @override + ClientChannelBase channel; + + @override + VisionServiceClient get client => VisionServiceClient(channel); + + VisionClient(this.name, this.channel); + + /// Get a list of [Detection]s from the camera named [cameraName]. + Future> detectionsFromCamera(String cameraName, {Map? extra}) async { + final request = GetDetectionsFromCameraRequest(name: name, cameraName: cameraName, extra: extra?.toStruct()); + final response = await client.getDetectionsFromCamera(request); + return response.detections; + } + + /// Get a list of [Detection]s from the provided [image]. + Future> detections(ViamImage image, {Map? extra}) async { + final request = GetDetectionsRequest( + name: name, + image: image.raw, + width: Int64(image.image?.width ?? 0), + height: Int64(image.image?.height ?? 0), + mimeType: image.mimeType.name, + extra: extra?.toStruct()); + final response = await client.getDetections(request); + return response.detections; + } + + /// Get a list of [Classification]s from the camera named [cameraName]. + /// The maximum number of [Classification]s returned is [count]. + Future> classificationsFromCamera(String cameraName, int count, {Map? extra}) async { + final request = GetClassificationsFromCameraRequest(name: name, cameraName: cameraName, n: count, extra: extra?.toStruct()); + final response = await client.getClassificationsFromCamera(request); + return response.classifications; + } + + /// Get a list of [Classification]s from the provided [image]. + /// The maximum number of [Classification]s returned is [count]. + Future> classifications(ViamImage image, int count, {Map? extra}) async { + final request = GetClassificationsRequest( + name: name, + image: image.raw, + width: image.image?.width, + height: image.image?.height, + mimeType: image.mimeType.name, + n: count, + extra: extra?.toStruct()); + final response = await client.getClassifications(request); + return response.classifications; + } + + /// Get a list of [PointCloudObject]s from the camera named [cameraName]. + Future> objectPointClouds(String cameraName, {Map? extra}) async { + final request = GetObjectPointCloudsRequest(name: name, cameraName: cameraName, mimeType: MimeType.pcd.name, extra: extra?.toStruct()); + final response = await client.getObjectPointClouds(request); + return response.objects; + } + + /// Send/Receive arbitrary commands to the Resource + Future> doCommand(Map command) async { + final request = DoCommandRequest() + ..name = name + ..command = command.toStruct(); + final response = await client.doCommand(request); + return response.result.toMap(); + } +} diff --git a/lib/viam_sdk.dart b/lib/viam_sdk.dart index eff12412ddc..3af19187361 100644 --- a/lib/viam_sdk.dart +++ b/lib/viam_sdk.dart @@ -49,5 +49,8 @@ export 'src/robot/client.dart'; /// RPC export 'src/rpc/dial.dart'; +/// Services +export 'src/services/vision.dart'; + /// Misc export 'src/viam_sdk.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 7f394f00d97..0acfbeb758b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - flutter_webrtc: ^0.9.47 + flutter_webrtc: ^0.10.3 grpc: ^3.2.3 protobuf: ^3.0.0 image: ^4.0.16 diff --git a/test/unit_test/app/app_client_test.dart b/test/unit_test/app/app_client_test.dart index bf3f08913c5..db98714f5cd 100644 --- a/test/unit_test/app/app_client_test.dart +++ b/test/unit_test/app/app_client_test.dart @@ -13,7 +13,7 @@ void main() { late MockAppServiceClient serviceClient; late AppClient appClient; - setUp(() async { + setUp(() { serviceClient = MockAppServiceClient(); appClient = AppClient(serviceClient); }); diff --git a/test/unit_test/mocks/service_clients_mocks.dart b/test/unit_test/mocks/service_clients_mocks.dart index 10bfbcb42e0..3659bef4906 100644 --- a/test/unit_test/mocks/service_clients_mocks.dart +++ b/test/unit_test/mocks/service_clients_mocks.dart @@ -4,6 +4,7 @@ import 'package:viam_sdk/src/gen/app/data/v1/data.pbgrpc.dart'; import 'package:viam_sdk/src/gen/app/v1/app.pbgrpc.dart'; import 'package:viam_sdk/src/gen/provisioning/v1/provisioning.pbgrpc.dart'; import 'package:viam_sdk/src/gen/robot/v1/robot.pbgrpc.dart'; +import 'package:viam_sdk/src/gen/service/vision/v1/vision.pbgrpc.dart'; @GenerateNiceMocks([ MockSpec(), @@ -11,5 +12,6 @@ import 'package:viam_sdk/src/gen/robot/v1/robot.pbgrpc.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() {} diff --git a/test/unit_test/mocks/service_clients_mocks.mocks.dart b/test/unit_test/mocks/service_clients_mocks.mocks.dart index 7166cddcaa4..654c581a549 100644 --- a/test/unit_test/mocks/service_clients_mocks.mocks.dart +++ b/test/unit_test/mocks/service_clients_mocks.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.3 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in viam_sdk/test/unit_test/mocks/service_clients_mocks.dart. // Do not manually edit this file. @@ -15,11 +15,14 @@ import 'package:viam_sdk/src/gen/app/data/v1/data.pb.dart' as _i13; import 'package:viam_sdk/src/gen/app/data/v1/data.pbgrpc.dart' as _i12; import 'package:viam_sdk/src/gen/app/v1/app.pb.dart' as _i11; import 'package:viam_sdk/src/gen/app/v1/app.pbgrpc.dart' as _i10; +import 'package:viam_sdk/src/gen/common/v1/common.pb.dart' as _i18; import 'package:viam_sdk/src/gen/provisioning/v1/provisioning.pb.dart' as _i15; import 'package:viam_sdk/src/gen/provisioning/v1/provisioning.pbgrpc.dart' as _i14; import 'package:viam_sdk/src/gen/robot/v1/robot.pb.dart' as _i9; import 'package:viam_sdk/src/gen/robot/v1/robot.pbgrpc.dart' as _i8; +import 'package:viam_sdk/src/gen/service/vision/v1/vision.pb.dart' as _i17; +import 'package:viam_sdk/src/gen/service/vision/v1/vision.pbgrpc.dart' as _i16; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -649,6 +652,65 @@ class MockRobotServiceClient extends _i1.Mock ), ) as _i4.ResponseFuture<_i9.SendSessionHeartbeatResponse>); + @override + _i4.ResponseFuture<_i9.LogResponse> log( + _i9.LogRequest? request, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #log, + [request], + {#options: options}, + ), + returnValue: _FakeResponseFuture_2<_i9.LogResponse>( + this, + Invocation.method( + #log, + [request], + {#options: options}, + ), + ), + returnValueForMissingStub: _FakeResponseFuture_2<_i9.LogResponse>( + this, + Invocation.method( + #log, + [request], + {#options: options}, + ), + ), + ) as _i4.ResponseFuture<_i9.LogResponse>); + + @override + _i4.ResponseFuture<_i9.GetCloudMetadataResponse> getCloudMetadata( + _i9.GetCloudMetadataRequest? request, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #getCloudMetadata, + [request], + {#options: options}, + ), + returnValue: _FakeResponseFuture_2<_i9.GetCloudMetadataResponse>( + this, + Invocation.method( + #getCloudMetadata, + [request], + {#options: options}, + ), + ), + returnValueForMissingStub: + _FakeResponseFuture_2<_i9.GetCloudMetadataResponse>( + this, + Invocation.method( + #getCloudMetadata, + [request], + {#options: options}, + ), + ), + ) as _i4.ResponseFuture<_i9.GetCloudMetadataResponse>); + @override _i3.ClientCall $createCall( _i7.ClientMethod? method, @@ -3968,3 +4030,310 @@ class MockProvisioningServiceClient extends _i1.Mock ), ) as _i4.ResponseStream); } + +/// A class which mocks [VisionServiceClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockVisionServiceClient extends _i1.Mock + implements _i16.VisionServiceClient { + @override + _i4.ResponseFuture<_i17.GetDetectionsFromCameraResponse> + getDetectionsFromCamera( + _i17.GetDetectionsFromCameraRequest? request, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #getDetectionsFromCamera, + [request], + {#options: options}, + ), + returnValue: + _FakeResponseFuture_2<_i17.GetDetectionsFromCameraResponse>( + this, + Invocation.method( + #getDetectionsFromCamera, + [request], + {#options: options}, + ), + ), + returnValueForMissingStub: + _FakeResponseFuture_2<_i17.GetDetectionsFromCameraResponse>( + this, + Invocation.method( + #getDetectionsFromCamera, + [request], + {#options: options}, + ), + ), + ) as _i4.ResponseFuture<_i17.GetDetectionsFromCameraResponse>); + + @override + _i4.ResponseFuture<_i17.GetDetectionsResponse> getDetections( + _i17.GetDetectionsRequest? request, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #getDetections, + [request], + {#options: options}, + ), + returnValue: _FakeResponseFuture_2<_i17.GetDetectionsResponse>( + this, + Invocation.method( + #getDetections, + [request], + {#options: options}, + ), + ), + returnValueForMissingStub: + _FakeResponseFuture_2<_i17.GetDetectionsResponse>( + this, + Invocation.method( + #getDetections, + [request], + {#options: options}, + ), + ), + ) as _i4.ResponseFuture<_i17.GetDetectionsResponse>); + + @override + _i4.ResponseFuture< + _i17.GetClassificationsFromCameraResponse> getClassificationsFromCamera( + _i17.GetClassificationsFromCameraRequest? request, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #getClassificationsFromCamera, + [request], + {#options: options}, + ), + returnValue: + _FakeResponseFuture_2<_i17.GetClassificationsFromCameraResponse>( + this, + Invocation.method( + #getClassificationsFromCamera, + [request], + {#options: options}, + ), + ), + returnValueForMissingStub: + _FakeResponseFuture_2<_i17.GetClassificationsFromCameraResponse>( + this, + Invocation.method( + #getClassificationsFromCamera, + [request], + {#options: options}, + ), + ), + ) as _i4.ResponseFuture<_i17.GetClassificationsFromCameraResponse>); + + @override + _i4.ResponseFuture<_i17.GetClassificationsResponse> getClassifications( + _i17.GetClassificationsRequest? request, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #getClassifications, + [request], + {#options: options}, + ), + returnValue: _FakeResponseFuture_2<_i17.GetClassificationsResponse>( + this, + Invocation.method( + #getClassifications, + [request], + {#options: options}, + ), + ), + returnValueForMissingStub: + _FakeResponseFuture_2<_i17.GetClassificationsResponse>( + this, + Invocation.method( + #getClassifications, + [request], + {#options: options}, + ), + ), + ) as _i4.ResponseFuture<_i17.GetClassificationsResponse>); + + @override + _i4.ResponseFuture<_i17.GetObjectPointCloudsResponse> getObjectPointClouds( + _i17.GetObjectPointCloudsRequest? request, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #getObjectPointClouds, + [request], + {#options: options}, + ), + returnValue: _FakeResponseFuture_2<_i17.GetObjectPointCloudsResponse>( + this, + Invocation.method( + #getObjectPointClouds, + [request], + {#options: options}, + ), + ), + returnValueForMissingStub: + _FakeResponseFuture_2<_i17.GetObjectPointCloudsResponse>( + this, + Invocation.method( + #getObjectPointClouds, + [request], + {#options: options}, + ), + ), + ) as _i4.ResponseFuture<_i17.GetObjectPointCloudsResponse>); + + @override + _i4.ResponseFuture<_i18.DoCommandResponse> doCommand( + _i18.DoCommandRequest? request, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #doCommand, + [request], + {#options: options}, + ), + returnValue: _FakeResponseFuture_2<_i18.DoCommandResponse>( + this, + Invocation.method( + #doCommand, + [request], + {#options: options}, + ), + ), + returnValueForMissingStub: + _FakeResponseFuture_2<_i18.DoCommandResponse>( + this, + Invocation.method( + #doCommand, + [request], + {#options: options}, + ), + ), + ) as _i4.ResponseFuture<_i18.DoCommandResponse>); + + @override + _i3.ClientCall $createCall( + _i7.ClientMethod? method, + _i6.Stream? requests, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #$createCall, + [ + method, + requests, + ], + {#options: options}, + ), + returnValue: _FakeClientCall_1( + this, + Invocation.method( + #$createCall, + [ + method, + requests, + ], + {#options: options}, + ), + ), + returnValueForMissingStub: _FakeClientCall_1( + this, + Invocation.method( + #$createCall, + [ + method, + requests, + ], + {#options: options}, + ), + ), + ) as _i3.ClientCall); + + @override + _i4.ResponseFuture $createUnaryCall( + _i7.ClientMethod? method, + Q? request, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #$createUnaryCall, + [ + method, + request, + ], + {#options: options}, + ), + returnValue: _FakeResponseFuture_2( + this, + Invocation.method( + #$createUnaryCall, + [ + method, + request, + ], + {#options: options}, + ), + ), + returnValueForMissingStub: _FakeResponseFuture_2( + this, + Invocation.method( + #$createUnaryCall, + [ + method, + request, + ], + {#options: options}, + ), + ), + ) as _i4.ResponseFuture); + + @override + _i4.ResponseStream $createStreamingCall( + _i7.ClientMethod? method, + _i6.Stream? requests, { + _i3.CallOptions? options, + }) => + (super.noSuchMethod( + Invocation.method( + #$createStreamingCall, + [ + method, + requests, + ], + {#options: options}, + ), + returnValue: _FakeResponseStream_3( + this, + Invocation.method( + #$createStreamingCall, + [ + method, + requests, + ], + {#options: options}, + ), + ), + returnValueForMissingStub: _FakeResponseStream_3( + this, + Invocation.method( + #$createStreamingCall, + [ + method, + requests, + ], + {#options: options}, + ), + ), + ) as _i4.ResponseStream); +} diff --git a/test/unit_test/services/vision_test.dart b/test/unit_test/services/vision_test.dart new file mode 100644 index 00000000000..da8fb451764 --- /dev/null +++ b/test/unit_test/services/vision_test.dart @@ -0,0 +1,82 @@ +import 'package:fixnum/fixnum.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:viam_sdk/protos/service/vision.dart'; +import 'package:viam_sdk/src/gen/common/v1/common.pb.dart'; +import 'package:viam_sdk/src/gen/service/vision/v1/vision.pbgrpc.dart'; +import 'package:viam_sdk/viam_sdk.dart'; + +import '../mocks/mock_response_future.dart'; +import '../mocks/service_clients_mocks.mocks.dart'; + +class FakeVisionClient extends VisionClient { + @override + VisionServiceClient get client => _client; + + final MockVisionServiceClient _client; + + FakeVisionClient(super.name, super.channel, this._client); +} + +void main() { + late VisionClient client; + late MockVisionServiceClient serviceClient; + + setUp(() { + serviceClient = MockVisionServiceClient(); + client = FakeVisionClient('vision', MockClientChannelBase(), serviceClient); + }); + + group('Vision RPC Client Tests', () { + test('getDetectionsFromCamera', () async { + final expected = [Detection(xMin: Int64(1), xMax: Int64(2), yMin: Int64(3), yMax: Int64(4), confidence: 0.95, className: 'test')]; + when(serviceClient.getDetectionsFromCamera(any)) + .thenAnswer((_) => MockResponseFuture.value(GetDetectionsFromCameraResponse(detections: expected))); + final response = await client.detectionsFromCamera('cameraName'); + expect(response, equals(expected)); + }); + + test('getDetections', () async { + final expected = [Detection(xMin: Int64(1), xMax: Int64(2), yMin: Int64(3), yMax: Int64(4), confidence: 0.95, className: 'test')]; + when(serviceClient.getDetections(any)).thenAnswer((_) => MockResponseFuture.value(GetDetectionsResponse(detections: expected))); + final response = await client.detections(ViamImage([1, 2, 3], const MimeType.unsupported('fake'))); + expect(response, equals(expected)); + }); + + test('getClassificationsFromCamera', () async { + final expected = [Classification(className: 'test-classification', confidence: 0.44)]; + when(serviceClient.getClassificationsFromCamera(any)) + .thenAnswer((_) => MockResponseFuture.value(GetClassificationsFromCameraResponse(classifications: expected))); + final response = await client.classificationsFromCamera('cameraName', 2); + expect(response, equals(expected)); + }); + + test('getClassifications', () async { + final expected = [Classification(className: 'test-classification', confidence: 0.44)]; + when(serviceClient.getClassifications(any)) + .thenAnswer((_) => MockResponseFuture.value(GetClassificationsResponse(classifications: expected))); + final response = await client.classifications(ViamImage([1, 2, 3], const MimeType.unsupported('fake')), 2); + expect(response, equals(expected)); + }); + + test('getObjectPointClouds', () async { + final expected = [ + PointCloudObject( + pointCloud: [1, 2, 3, 4, 5, 6], + geometries: GeometriesInFrame( + referenceFrame: 'ref-frame', + geometries: [ + Geometry( + center: Pose(x: 1, y: 2, z: 3, oX: 4, oY: 5, oZ: 6, theta: 7), + ), + ], + ), + ), + ]; + when(serviceClient.getObjectPointClouds(any)) + .thenAnswer((_) => MockResponseFuture.value(GetObjectPointCloudsResponse(objects: expected, mimeType: MimeType.pcd.name))); + final response = await client.objectPointClouds('cameraName'); + expect(response, equals(expected)); + }); + }); +}