Skip to content

Commit 9708793

Browse files
nicksanfordnjooma
authored andcommitted
RSDK-6011 - Add Motion Expose Paths project RPC methods (#500)
1 parent ad78e09 commit 9708793

File tree

3 files changed

+411
-9
lines changed

3 files changed

+411
-9
lines changed

src/viam/services/motion/client.py

Lines changed: 176 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,24 @@
1515
)
1616
from viam.proto.service.motion import (
1717
Constraints,
18+
GetPlanRequest,
19+
GetPlanResponse,
1820
GetPoseRequest,
1921
GetPoseResponse,
22+
ListPlanStatusesRequest,
23+
ListPlanStatusesResponse,
2024
MotionConfiguration,
2125
MotionServiceStub,
26+
MoveOnGlobeNewRequest,
27+
MoveOnGlobeNewResponse,
2228
MoveOnGlobeRequest,
2329
MoveOnGlobeResponse,
2430
MoveOnMapRequest,
2531
MoveOnMapResponse,
2632
MoveRequest,
2733
MoveResponse,
34+
StopPlanRequest,
35+
StopPlanResponse,
2836
)
2937
from viam.resource.rpc_client_base import ReconfigurableResourceRPCClientBase
3038
from viam.resource.types import RESOURCE_NAMESPACE_RDK, RESOURCE_TYPE_SERVICE, Subtype
@@ -108,10 +116,10 @@ async def move_on_globe(
108116
component_name (ResourceName): The component to move
109117
destination (GeoPoint): The destination point
110118
movement_sensor_name (ResourceName): The ``MovementSensor`` which will be used to check robot location
111-
obstacles (Optional[Sequence[GeoObstacle]], optional): Obstacles to be considered for motion planning. Defaults to None.
112-
heading (Optional[float], optional): Compass heading to achieve at the destination, in degrees [0-360). Defaults to None.
113-
linear_meters_per_sec (Optional[float], optional): Linear velocity to target when moving. Defaults to None.
114-
angular_deg_per_sec (Optional[float], optional): Angular velocity to target when turning. Defaults to None.
119+
obstacles (Optional[Sequence[GeoObstacle]]): Obstacles to be considered for motion planning. Defaults to None.
120+
heading (Optional[float]): Compass heading to achieve at the destination, in degrees [0-360). Defaults to None.
121+
linear_meters_per_sec (Optional[float]): Linear velocity to target when moving. Defaults to None.
122+
angular_deg_per_sec (Optional[float]): Angular velocity to target when turning. Defaults to None.
115123
116124
Returns:
117125
bool: Whether the request was successful
@@ -131,6 +139,57 @@ async def move_on_globe(
131139
response: MoveOnGlobeResponse = await self.client.MoveOnGlobe(request, timeout=timeout)
132140
return response.success
133141

142+
async def move_on_globe_new(
143+
self,
144+
component_name: ResourceName,
145+
destination: GeoPoint,
146+
movement_sensor_name: ResourceName,
147+
obstacles: Optional[Sequence[GeoObstacle]] = None,
148+
heading: Optional[float] = None,
149+
configuration: Optional[MotionConfiguration] = None,
150+
*,
151+
extra: Optional[Mapping[str, ValueTypes]] = None,
152+
timeout: Optional[float] = None,
153+
) -> str:
154+
"""
155+
**Experimental**: use move_on_globe instead.
156+
Move a component to a specific latitude and longitude, using a ``MovementSensor`` to check the location.
157+
158+
``move_on_globe_new()`` is non blocking, meaning the motion service will move the component to the destination
159+
GPS point after ``move_on_globe_new()`` returns.
160+
161+
Each successful ``move_on_globe_new()`` call retuns a unique ExectionID which you can use to identify all plans
162+
generated durring the ``move_on_globe_new()`` call.
163+
164+
You can monitor the progress of the ``move_on_globe_new()`` call by querying ``get_plan()`` and ``list_plan_statuses()``.
165+
166+
Args:
167+
component_name (ResourceName): The component to move
168+
destination (GeoPoint): The destination point
169+
movement_sensor_name (ResourceName): The ``MovementSensor`` which will be used to check robot location
170+
obstacles (Optional[Sequence[GeoObstacle]]): Obstacles to be considered for motion planning. Defaults to None.
171+
heading (Optional[float]): Compass heading to achieve at the destination, in degrees [0-360]. Defaults to None.
172+
linear_meters_per_sec (Optional[float]): Linear velocity to target when moving. Defaults to None.
173+
angular_deg_per_sec (Optional[float]): Angular velocity to target when turning. Defaults to None.
174+
175+
Returns:
176+
str: ExecutionID of the move_on_globe_new call, which can be used to track execution progress.
177+
"""
178+
if extra is None:
179+
extra = {}
180+
request = MoveOnGlobeNewRequest(
181+
name=self.name,
182+
component_name=component_name,
183+
destination=destination,
184+
movement_sensor_name=movement_sensor_name,
185+
obstacles=obstacles,
186+
heading=heading,
187+
motion_configuration=configuration,
188+
extra=dict_to_struct(extra),
189+
)
190+
response: MoveOnGlobeNewResponse = await self.client.MoveOnGlobeNew(request, timeout=timeout)
191+
return response.execution_id
192+
134193
async def move_on_map(
135194
self,
136195
component_name: ResourceName,
@@ -140,7 +199,8 @@ async def move_on_map(
140199
extra: Optional[Mapping[str, ValueTypes]] = None,
141200
timeout: Optional[float] = None,
142201
) -> bool:
143-
"""Move a component to a specific pose, using a ``SlamService`` for the SLAM map
202+
"""
203+
Move a component to a specific pose, using a ``SlamService`` for the SLAM map
144204
145205
Args:
146206
component_name (ResourceName): The component to move
@@ -162,6 +222,117 @@ async def move_on_map(
162222
response: MoveOnMapResponse = await self.client.MoveOnMap(request, timeout=timeout)
163223
return response.success
164224

225+
async def stop_plan(
226+
self,
227+
component_name: ResourceName,
228+
*,
229+
extra: Optional[Mapping[str, ValueTypes]] = None,
230+
timeout: Optional[float] = None,
231+
):
232+
"""**Experimental**
233+
Stop a component being moved by an in progress ``move_on_globe_new()`` call.
234+
235+
Args:
236+
component_name (ResourceName): The component to stop
237+
238+
Returns:
239+
None
240+
"""
241+
if extra is None:
242+
extra = {}
243+
244+
request = StopPlanRequest(
245+
name=self.name,
246+
component_name=component_name,
247+
extra=dict_to_struct(extra),
248+
)
249+
_: StopPlanResponse = await self.client.StopPlan(request, timeout=timeout)
250+
return
251+
252+
async def get_plan(
253+
self,
254+
component_name: ResourceName,
255+
last_plan_only: bool = False,
256+
execution_id: Optional[str] = None,
257+
*,
258+
extra: Optional[Mapping[str, ValueTypes]] = None,
259+
timeout: Optional[float] = None,
260+
) -> GetPlanResponse:
261+
"""**Experimental**
262+
By default: returns the plan history of the most recent ``move_on_globe_new()`` call to move a component.
263+
264+
The plan history for executions before the most recent can be requested by providing an ExecutionID in the request.
265+
266+
Returns a result if both of the following conditions are met:
267+
268+
- the execution (call to ``move_on_globe_new()``) is still executing **or** changed state within the last 24 hours
269+
- the robot has not reinitialized
270+
271+
Plans never change.
272+
273+
Replans always create new plans.
274+
275+
Replans share the ExecutionID of the previously executing plan.
276+
277+
All repeated fields are in time ascending order.
278+
279+
Args:
280+
component_name (ResourceName): The component to stop
281+
last_plan_only (Optional[bool]): If supplied, the response will only return the last plan for the component / execution
282+
execution_id (Optional[str]): If supplied, the response will only return plans with the provided execution_id
283+
284+
Returns:
285+
``GetPlanResponse`` (GetPlanResponse): The current PlanWithStatus & replan history which matches the request
286+
"""
287+
if extra is None:
288+
extra = {}
289+
290+
if last_plan_only is None:
291+
last_plan_only = False
292+
request = GetPlanRequest(
293+
name=self.name,
294+
component_name=component_name,
295+
last_plan_only=last_plan_only,
296+
execution_id=execution_id,
297+
extra=dict_to_struct(extra),
298+
)
299+
response: GetPlanResponse = await self.client.GetPlan(request, timeout=timeout)
300+
return response
301+
302+
async def list_plan_statuses(
303+
self,
304+
only_active_plans: bool = False,
305+
*,
306+
extra: Optional[Mapping[str, ValueTypes]] = None,
307+
timeout: Optional[float] = None,
308+
) -> ListPlanStatusesResponse:
309+
"""**Experimental**
310+
Returns the statuses of plans created by `move_on_globe_new()` calls that meet at least one of the following
311+
conditions since the motion service initialized:
312+
313+
- the plan's status is in progress
314+
- the plan's status changed state within the last 24 hours
315+
316+
All repeated fields are in chronological order.
317+
318+
Args:
319+
only_active_plans (Optional[bool]): If supplied, the response will filter out any plans that are not executing
320+
321+
Returns:
322+
``ListPlanStatusesResponse`` (ListPlanStatusesResponse): List of last known statuses with the
323+
associated IDs of all plans within the TTL ordered by timestamp in ascending order
324+
"""
325+
if extra is None:
326+
extra = {}
327+
328+
request = ListPlanStatusesRequest(
329+
name=self.name,
330+
only_active_plans=only_active_plans,
331+
extra=dict_to_struct(extra),
332+
)
333+
response: ListPlanStatusesResponse = await self.client.ListPlanStatuses(request, timeout=timeout)
334+
return response
335+
165336
async def get_pose(
166337
self,
167338
component_name: ResourceName,

tests/mocks/services.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -443,9 +443,13 @@ def __init__(
443443
self,
444444
move_responses: Dict[str, bool],
445445
get_pose_responses: Dict[str, PoseInFrame],
446+
get_plan_response: GetPlanResponse,
447+
list_plan_statuses_response: ListPlanStatusesResponse
446448
):
447449
self.move_responses = move_responses
448450
self.get_pose_responses = get_pose_responses
451+
self.get_plan_response = get_plan_response
452+
self.list_plan_statuses_response = list_plan_statuses_response
449453
self.constraints: Optional[Constraints] = None
450454
self.extra: Optional[Mapping[str, Any]] = None
451455
self.timeout: Optional[float] = None
@@ -495,16 +499,45 @@ async def GetPose(self, stream: Stream[GetPoseRequest, GetPoseResponse]) -> None
495499
await stream.send_message(response)
496500

497501
async def MoveOnGlobeNew(self, stream: Stream[MoveOnGlobeNewRequest, MoveOnGlobeNewResponse]) -> None:
498-
raise NotImplementedError()
502+
request = await stream.recv_message()
503+
assert request is not None
504+
self.component_name = request.component_name
505+
self.destination = request.destination
506+
self.movement_sensor = request.movement_sensor_name
507+
self.obstacles = request.obstacles
508+
self.heading = request.heading
509+
self.configuration = request.motion_configuration
510+
self.extra = struct_to_dict(request.extra)
511+
self.timeout = stream.deadline.time_remaining() if stream.deadline else None
512+
self.execution_id = "some_execution_id"
513+
await stream.send_message(MoveOnGlobeNewResponse(execution_id=self.execution_id))
499514

500515
async def StopPlan(self, stream: Stream[StopPlanRequest, StopPlanResponse]) -> None:
501-
raise NotImplementedError()
516+
request = await stream.recv_message()
517+
assert request is not None
518+
self.component_name = request.component_name
519+
self.extra = struct_to_dict(request.extra)
520+
self.timeout = stream.deadline.time_remaining() if stream.deadline else None
521+
await stream.send_message(StopPlanResponse())
502522

503523
async def ListPlanStatuses(self, stream: Stream[ListPlanStatusesRequest, ListPlanStatusesResponse]) -> None:
504-
raise NotImplementedError()
524+
request = await stream.recv_message()
525+
assert request is not None
526+
self.only_active_plans = request.only_active_plans
527+
self.extra = struct_to_dict(request.extra)
528+
self.timeout = stream.deadline.time_remaining() if stream.deadline else None
529+
response = self.list_plan_statuses_response
530+
await stream.send_message(response)
505531

506532
async def GetPlan(self, stream: Stream[GetPlanRequest, GetPlanResponse]) -> None:
507-
raise NotImplementedError()
533+
request = await stream.recv_message()
534+
assert request is not None
535+
self.component_name = request.component_name
536+
self.last_plan_only = request.last_plan_only
537+
self.execution_id = request.execution_id
538+
self.extra = struct_to_dict(request.extra)
539+
self.timeout = stream.deadline.time_remaining() if stream.deadline else None
540+
await stream.send_message(self.get_plan_response)
508541

509542
async def DoCommand(self, stream: Stream[DoCommandRequest, DoCommandResponse]) -> None:
510543
request = await stream.recv_message()

0 commit comments

Comments
 (0)