diff --git a/lib/src/components/arm/arm.dart b/lib/src/components/arm/arm.dart index 94657f11fbb..6a34bb2e792 100644 --- a/lib/src/components/arm/arm.dart +++ b/lib/src/components/arm/arm.dart @@ -1,6 +1,5 @@ +import '../../../viam_sdk.dart'; import '../../gen/common/v1/common.pb.dart'; -import '../../resource/base.dart'; -import '../../robot/client.dart'; /// {@category Components} /// Arm represents a physical robot arm that exists in three-dimensional space. @@ -80,6 +79,15 @@ abstract class Arm extends Resource { /// For more information, see [Arm component](https://docs.viam.com/dev/reference/apis/components/arm/#ismoving). Future isMoving(); + /// Get the kinematics data associated with the [Arm] + /// + /// ``` + /// var kinematics = await myArm.getKinematics(); + /// ``` + /// + /// For more information, see [Arm component](https://docs.viam.com/dev/reference/apis/components/arm/#getkinematics). + Future getKinematics({Map? extra}); + /// Get the [ResourceName] for this [Arm] with the given [name]. /// /// ``` diff --git a/lib/src/components/arm/client.dart b/lib/src/components/arm/client.dart index 1822e07786e..3e948769a42 100644 --- a/lib/src/components/arm/client.dart +++ b/lib/src/components/arm/client.dart @@ -5,6 +5,7 @@ import '../../gen/component/arm/v1/arm.pbgrpc.dart'; import '../../gen/google/protobuf/struct.pb.dart'; import '../../resource/base.dart'; import '../../utils.dart'; +import '../gripper/gripper.dart'; import 'arm.dart'; /// {@category Components} @@ -91,4 +92,13 @@ class ArmClient extends Arm with RPCDebugLoggerMixin implements ResourceRPCClien final response = await client.doCommand(request, options: callOptions); return response.result.toMap(); } + + @override + Future getKinematics({Map? extra}) async { + final request = GetKinematicsRequest() + ..name = name + ..extra = extra?.toStruct() ?? Struct(); + final response = await client.getKinematics(request, options: callOptions); + return Kinematics.fromProto(response); + } } diff --git a/lib/src/components/arm/service.dart b/lib/src/components/arm/service.dart index cc00fec6624..2565f9cbb14 100644 --- a/lib/src/components/arm/service.dart +++ b/lib/src/components/arm/service.dart @@ -84,9 +84,12 @@ class ArmService extends ArmServiceBase { } @override - Future getKinematics(ServiceCall call, GetKinematicsRequest request) { - // TODO: implement getKinematics - throw UnimplementedError(); + Future getKinematics(ServiceCall call, GetKinematicsRequest request) async { + final arm = _armFromManager(request.name); + final response = await arm.getKinematics(extra: request.extra.toMap()); + return GetKinematicsResponse() + ..format = response.format + ..kinematicsData = response.raw; } @override diff --git a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart index 1f048d1b935..2ff419ea9bb 100644 --- a/lib/widgets/resources/arm_widgets/joint_positions_widget.dart +++ b/lib/widgets/resources/arm_widgets/joint_positions_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import '../../../src/utils.dart'; import '../../../viam_sdk.dart' as viam; import '../arm.dart'; @@ -20,6 +21,7 @@ class JointPositionsWidget extends StatefulWidget { class _JointPositionsWidgetState extends State { List _jointValues = []; bool _isLive = false; + bool _isLoading = false; List _textControllers = []; @override @@ -39,21 +41,39 @@ class _JointPositionsWidgetState extends State { } Future _getJointPositions() async { - for (final controller in _textControllers) { - controller.dispose(); - } - - _jointValues = await widget.arm.jointPositions(); - _textControllers = List.generate( - _jointValues.length, - (index) => TextEditingController(text: _jointValues[index].toStringAsFixed(1)), - ); - if (mounted) { - setState(() {}); + if (_isLoading) return; + _isLoading = true; + try { + _jointValues = await widget.arm.jointPositions(); + + if (mounted) { + if (_textControllers.isEmpty || _textControllers.length != _jointValues.length) { + for (final controller in _textControllers) { + controller.dispose(); + } + _textControllers = List.generate( + _jointValues.length, + (index) => TextEditingController(text: _jointValues[index].toStringAsFixed(1)), + ); + } else { + for (int i = 0; i < _jointValues.length; i++) { + _textControllers[i].text = _jointValues[i].toStringAsFixed(1); + } + } + setState(() {}); + } + } catch (e) { + if (mounted) await showErrorDialog(context, title: 'An error occurred', error: e.toString()); + } finally { + if (mounted) { + _isLoading = false; + } } } void _updateJointValue(int index, double value) { + if (!mounted || _textControllers.isEmpty || index >= _textControllers.length) return; + const double minPosition = -359.0; const double maxPosition = 359.0; final clampedValue = value.clamp(minPosition, maxPosition); @@ -187,7 +207,7 @@ class _BuildJointControlRow extends StatelessWidget { textAlign: TextAlign.center, keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,1}')), + FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')), ], onSubmitted: onSubmitted, ), diff --git a/lib/widgets/resources/arm_widgets/pose_widget.dart b/lib/widgets/resources/arm_widgets/pose_widget.dart index 601c49750ce..0ccc5b1b59e 100644 --- a/lib/widgets/resources/arm_widgets/pose_widget.dart +++ b/lib/widgets/resources/arm_widgets/pose_widget.dart @@ -42,6 +42,7 @@ class _PoseWidgetState extends State { bool _isLive = false; bool _isGoingToPose = false; + bool _isLoading = false; Pose _controlValues = Pose(); _TextControlStruct? _textControllers; @@ -69,24 +70,46 @@ class _PoseWidgetState extends State { _textControllers!.oY.dispose(); _textControllers!.oZ.dispose(); _textControllers!.theta.dispose(); + _textControllers = null; } } Future _getStartPose() async { - _disposeControllers(); + if (_isLoading) return; + _isLoading = true; + try { + final startPose = await widget.arm.endPosition(); + if (mounted) { + _controlValues = startPose; - final startPose = await widget.arm.endPosition(); - _controlValues = startPose; - _textControllers = _TextControlStruct( - TextEditingController(text: _controlValues.x.toStringAsFixed(1)), - TextEditingController(text: _controlValues.y.toStringAsFixed(1)), - TextEditingController(text: _controlValues.z.toStringAsFixed(1)), - TextEditingController(text: _controlValues.oX.toStringAsFixed(1)), - TextEditingController(text: _controlValues.oY.toStringAsFixed(1)), - TextEditingController(text: _controlValues.oZ.toStringAsFixed(1)), - TextEditingController(text: _controlValues.theta.toStringAsFixed(1)), - ); - setState(() {}); + if (_textControllers == null) { + _textControllers = _TextControlStruct( + TextEditingController(text: _controlValues.x.toStringAsFixed(1)), + TextEditingController(text: _controlValues.y.toStringAsFixed(1)), + TextEditingController(text: _controlValues.z.toStringAsFixed(1)), + TextEditingController(text: _controlValues.oX.toStringAsFixed(1)), + TextEditingController(text: _controlValues.oY.toStringAsFixed(1)), + TextEditingController(text: _controlValues.oZ.toStringAsFixed(1)), + TextEditingController(text: _controlValues.theta.toStringAsFixed(1)), + ); + } else { + _textControllers!.x.text = _controlValues.x.toStringAsFixed(1); + _textControllers!.y.text = _controlValues.y.toStringAsFixed(1); + _textControllers!.z.text = _controlValues.z.toStringAsFixed(1); + _textControllers!.oX.text = _controlValues.oX.toStringAsFixed(1); + _textControllers!.oY.text = _controlValues.oY.toStringAsFixed(1); + _textControllers!.oZ.text = _controlValues.oZ.toStringAsFixed(1); + _textControllers!.theta.text = _controlValues.theta.toStringAsFixed(1); + } + setState(() {}); + } + } catch (e) { + if (mounted) await showErrorDialog(context, title: 'An error occurred', error: e.toString()); + } finally { + if (mounted) { + _isLoading = false; + } + } } Future _updatePose() async { @@ -109,6 +132,8 @@ class _PoseWidgetState extends State { } void _updateControlValue(String axis, TextEditingController textController, double value) { + if (!mounted || _textControllers == null) return; + setState(() { switch (axis) { case 'x': @@ -162,12 +187,16 @@ class _PoseWidgetState extends State { controller: _textControllers!.x, min: _minPosition, max: _maxPosition, - onValueChanged: (newValue) => _updateControlValue( - 'x', - _textControllers!.x, - newValue.clamp(_minPosition, _maxPosition), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + onValueChanged: (newValue) { + _updateControlValue( + 'x', + _textControllers!.x, + newValue.clamp(_minPosition, _maxPosition), + ); + if (_isLive) { + _updatePose(); + } + }, ), _BuildJointControlRow( label: 'Y', @@ -175,12 +204,16 @@ class _PoseWidgetState extends State { controller: _textControllers!.y, min: _minPosition, max: _maxPosition, - onValueChanged: (newValue) => _updateControlValue( - 'y', - _textControllers!.y, - newValue.clamp(_minPosition, _maxPosition), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + onValueChanged: (newValue) { + _updateControlValue( + 'y', + _textControllers!.y, + newValue.clamp(_minPosition, _maxPosition), + ); + if (_isLive) { + _updatePose(); + } + }, ), _BuildJointControlRow( label: 'Z', @@ -188,51 +221,67 @@ class _PoseWidgetState extends State { controller: _textControllers!.z, min: _minPosition, max: _maxPosition, - onValueChanged: (newValue) => _updateControlValue( - 'z', - _textControllers!.z, - newValue.clamp(_minPosition, _maxPosition), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + onValueChanged: (newValue) { + _updateControlValue( + 'z', + _textControllers!.z, + newValue.clamp(_minPosition, _maxPosition), + ); + if (_isLive) { + _updatePose(); + } + }, ), _BuildJointControlRow( label: 'OX', - value: _controlValues.oX.roundToDouble(), + value: _controlValues.oX, controller: _textControllers!.oX, min: _minOrientation, max: _maxOrientation, - onValueChanged: (newValue) => _updateControlValue( - 'oX', - _textControllers!.oX, - newValue.clamp(_minOrientation, _maxOrientation), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + onValueChanged: (newValue) { + _updateControlValue( + 'oX', + _textControllers!.oX, + newValue.clamp(_minOrientation, _maxOrientation), + ); + if (_isLive) { + _updatePose(); + } + }, ), _BuildJointControlRow( label: 'OY', - value: _controlValues.oY.roundToDouble(), + value: _controlValues.oY, controller: _textControllers!.oY, min: _minOrientation, max: _maxOrientation, - onValueChanged: (newValue) => _updateControlValue( - 'oY', - _textControllers!.oY, - newValue.clamp(_minOrientation, _maxOrientation), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + onValueChanged: (newValue) { + _updateControlValue( + 'oY', + _textControllers!.oY, + newValue.clamp(_minOrientation, _maxOrientation), + ); + if (_isLive) { + _updatePose(); + } + }, ), _BuildJointControlRow( label: 'OZ', - value: _controlValues.oZ.roundToDouble(), + value: _controlValues.oZ, controller: _textControllers!.oZ, min: _minOrientation, max: _maxOrientation, - onValueChanged: (newValue) => _updateControlValue( - 'oZ', - _textControllers!.oZ, - newValue.clamp(_minOrientation, _maxOrientation), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + onValueChanged: (newValue) { + _updateControlValue( + 'oZ', + _textControllers!.oZ, + newValue.clamp(_minOrientation, _maxOrientation), + ); + if (_isLive) { + _updatePose(); + } + }, ), _BuildJointControlRow( label: 'Theta', @@ -240,12 +289,16 @@ class _PoseWidgetState extends State { controller: _textControllers!.theta, min: _minTheta, max: _maxTheta, - onValueChanged: (newValue) => _updateControlValue( - 'theta', - _textControllers!.theta, - newValue.clamp(_minTheta, _maxTheta), - ), - onValueChangedEnd: (newValue) async => _isLive ? _updatePose() : () {}, + onValueChanged: (newValue) { + _updateControlValue( + 'theta', + _textControllers!.theta, + newValue.clamp(_minTheta, _maxTheta), + ); + if (_isLive) { + _updatePose(); + } + }, ), ], ), @@ -266,13 +319,6 @@ class _PoseWidgetState extends State { Text( 'Live', ), - Tooltip( - message: 'In Live mode, pose will update \non release of the slider', - textAlign: TextAlign.center, - triggerMode: TooltipTriggerMode.tap, - preferBelow: false, - child: Icon(Icons.info_outline), - ), Spacer(), OutlinedButton.icon( onPressed: _isLive ? null : _updatePose, @@ -294,7 +340,6 @@ class _BuildJointControlRow extends StatelessWidget { final double min; final double max; final ValueChanged onValueChanged; - final ValueChanged onValueChangedEnd; const _BuildJointControlRow({ required this.label, @@ -303,7 +348,6 @@ class _BuildJointControlRow extends StatelessWidget { required this.min, required this.max, required this.onValueChanged, - required this.onValueChangedEnd, }); @override @@ -328,28 +372,25 @@ class _BuildJointControlRow extends StatelessWidget { textAlign: TextAlign.center, keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^-?\d+\.?\d{0,1}')), + FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')), ], onSubmitted: (newValue) { final parsedValue = double.tryParse(newValue) ?? value; onValueChanged(parsedValue); - onValueChangedEnd(parsedValue); }, ), ), Spacer(), IconButton( icon: const Icon(Icons.remove), - onPressed: () async { + onPressed: () { onValueChanged(value - (max == 1 ? 0.1 : 1.0)); - onValueChangedEnd(value); }, ), IconButton( icon: const Icon(Icons.add), - onPressed: () async { + onPressed: () { onValueChanged(value + (max == 1 ? 0.1 : 1.0)); - onValueChangedEnd(value); }, ), ], @@ -359,12 +400,10 @@ class _BuildJointControlRow extends StatelessWidget { SizedBox(width: 35), Expanded( child: Slider( - value: value, + value: value.clamp(min, max), min: min, max: max, - label: value.toStringAsFixed(1), onChanged: onValueChanged, - onChangeEnd: onValueChangedEnd, ), ), ], diff --git a/test/unit_test/components/arm_test.dart b/test/unit_test/components/arm_test.dart index fdf49e3d643..b3c992a7d31 100644 --- a/test/unit_test/components/arm_test.dart +++ b/test/unit_test/components/arm_test.dart @@ -18,6 +18,7 @@ class FakeArm extends Arm { ..z = 0; Map? extra; Map arm3DModels = {}; + Kinematics armKinematics = Kinematics(KinematicsFileFormat.KINEMATICS_FILE_FORMAT_SVA, [1, 2, 3]); @override String name; @@ -72,6 +73,12 @@ class FakeArm extends Arm { this.extra = extra; return arm3DModels; } + + @override + Future getKinematics({Map? extra}) async { + this.extra = extra; + return armKinematics; + } } void main() { @@ -146,6 +153,12 @@ void main() { await arm.stop(extra: {'foo': 'bar'}); expect(arm.extra, {'foo': 'bar'}); }); + + test('getKinematics', () async { + final kinematics = await arm.getKinematics(); + expect(kinematics.format, KinematicsFileFormat.KINEMATICS_FILE_FORMAT_SVA); + expect(kinematics.raw, [1, 2, 3]); + }); }); group('Arm RPC Tests', () { @@ -264,6 +277,14 @@ void main() { ..extra = {'foo': 'bar'}.toStruct()); expect(arm.extra, {'foo': 'bar'}); }); + + test('getKinematics', () async { + final client = ArmServiceClient(channel); + final request = GetKinematicsRequest()..name = name; + final response = await client.getKinematics(request); + expect(response.format, KinematicsFileFormat.KINEMATICS_FILE_FORMAT_SVA); + expect(response.kinematicsData, [1, 2, 3]); + }); }); group('Arm Client Tests', () { @@ -338,5 +359,11 @@ void main() { expect(arm.extra, {'foo': 'bar'}); }); }); + test('getKinematics', () async { + final client = ArmClient(name, channel); + final kinematics = await client.getKinematics(); + expect(kinematics.format, KinematicsFileFormat.KINEMATICS_FILE_FORMAT_SVA); + expect(kinematics.raw, [1, 2, 3]); + }); }); }