Skip to content

Add move_through_joint_positions and get_3d_models to Arm (BREAKING) - #1250

Open
Nick Hehr (HipsterBrown) wants to merge 7 commits into
mainfrom
arm-trajectory-3d-models
Open

Add move_through_joint_positions and get_3d_models to Arm (BREAKING)#1250
Nick Hehr (HipsterBrown) wants to merge 7 commits into
mainfrom
arm-trajectory-3d-models

Conversation

@HipsterBrown

Copy link
Copy Markdown
Member

⚠️ Breaking change — requires a release note

Arm now declares two new @abc.abstractmethods. Every existing third-party arm module will fail at startup until it implements both:

TypeError: Can't instantiate abstract class MyArm with abstract methods
get_3d_models, move_through_joint_positions

This is not a deprecation or a runtime warning — the module does not start. The release carrying this needs an explicit upgrade note, since the TypeError gives module authors no hint about what to implement.

Abstract (rather than concrete-raising-MethodNotImplementedError) was a deliberate choice for consistency with the other seven Arm methods.

Summary

Brings the Python SDK's Arm to RPC parity with the Go SDK (rdk) by implementing the two arm RPCs the SDK did not expose:

  • move_through_joint_positions(positions, options=None, ...) — executes a multi-waypoint joint trajectory under optional kinematic ceilings (MoveOptions). Previously the only way to do this from Python was a move_to_joint_positions loop with no velocity or acceleration control.
  • get_3d_models() -> Mapping[str, Mesh] — retrieves the arm's 3D meshes.

Both are implemented across all three layers (ABC, ArmRPCService, ArmClient), plus all seven in-repo Arm subclasses. MoveOptions and Mesh are now re-exported from viam.components.arm.

No proto regeneration — the generated stubs and types already existed. src/viam/gen/** and src/viam/proto/** are untouched.

Coverage before/after

RPC proto Go RDK Python (before) Python (after)
GetEndPosition, MoveToPosition, GetJointPositions, MoveToJointPositions, Stop, IsMoving, DoCommand, GetStatus, GetKinematics, GetGeometries
MoveThroughJointPositions
Get3DModels
MoveThroughJointPositionsStreamed ❌ (intentional)

Notes for reviewers

MoveOptions presence is two levels deep. The options field has explicit presence, so the service branches on request.HasField("options") — without it, a caller who omitted options would hand the driver a zeroed MoveOptions, and max_vel_degs_per_sec == 0.0 could be misread as "do not move" rather than "no limit requested". The scalars inside MoveOptions have presence too, so the ABC docstring directs implementers to check options.HasField(...) per field. It also documents the proto's precedence rule (max_vel_degs_per_sec is ignored when max_vel_degs_per_sec_joints is set), which nothing else in the SDK surfaced.

get_3d_models returns dict(response.models), not the raw container. Protobuf map fields mimic C++ semantics — __getitem__ on a missing key default-constructs and inserts rather than raising KeyError. Returning the live container would mean models["gripper_link"] on an absent link yields an empty Mesh and silently grows the caller's mapping, breaking the declared Mapping[str, Mesh] contract. A test pins the KeyError.

No client-side joint-limit validation, unlike Go. rdk's client calls Kinematics() and runs checkDesiredJointPositions before every MoveThroughJointPositions. Python has no equivalent of referenceframe.Modelget_kinematics() returns raw URDF/SVA bytes with no parser — so there is nothing to validate against. The asymmetry is documented in the ABC docstring's Note:: the Go client refuses out-of-limit waypoints, the Python client forwards them to the driver. Adding a parser is its own project.

MoveThroughJointPositionsStreamed deliberately left unimplemented — it exists in the generated stubs but is implemented in neither rdk nor here.

Known follow-up (not in this PR)

get_kinematics returns response.meshes_by_urdf_filepath — the same live protobuf container, with the same missing-key auto-insert bug described above. Pre-existing, so it was left out of scope here, but it warrants a separate fix.

Test Plan

  • make test — 876 passed, 0 failed (includes tests/test_examples.py, which launches the server and complex_module example modules)
  • make typecheck — 0 errors (1 pre-existing warning in src/viam/module/types.py)
  • make lint / make format — clean, 301 files
  • git diff --stat -- src/viam/gen src/viam/proto — empty
  • Arm.__abstractmethods__ contains both new methods; ArmClient.__abstractmethods__ is empty
  • End-to-end over ChannelFor: waypoints and MoveOptions round-trip intact; omitted options arrive as None rather than zeroed; get_3d_models returns a real dict that raises KeyError without inserting
  • All seven Arm subclasses instantiate — verified in subprocesses for the example/docs arms, which no lint, typecheck, or test covers
  • docs/examples/example.ipynb — valid JSON, cell sources still list-of-lines, embedded MyModularArm parses

10 new tests across TestArm, TestService, and TestClient.

🤖 Generated with Claude Code

https://claude.ai/code/session_015RbNNgUjePEdPuUGAdU3qb

@github-actions

Copy link
Copy Markdown
Contributor

Warning your change may break code samples. If your change modifies any of the following functions please contact @viamrobotics/fleet-management. Thanks!

component function
base is_moving
board gpio_pin_by_name
button push
genericcomponent do_command
camera get_images
encoder get_position
motor is_moving
sensor get_readings
servo get_position
switch get_position
arm get_end_position
gantry get_lengths
gripper is_moving
movement_sensor get_linear_acceleration
input_controller get_controls
audio get_properties
pose_tracker get_poses
power_sensor get_power
motion get_pose
vision get_properties
mlmodel metadata
genericservice do_command
slam get_point_cloud_map
audio_in get_properties
audio_out get_properties

@viambot

viambot commented Jul 29, 2026

Copy link
Copy Markdown
Member

👋 Thanks for requesting a review from the team!

We aim to review PRs within one business day. If this is urgent
or blocking you, please reach out in #team-sdk and
we'll prioritize it.

Nick Hehr (HipsterBrown) and others added 7 commits July 29, 2026 15:34
…on all Arm subclasses

Adds both methods to MockArm, ExampleArm, MyArm, MyCoolArm, and MyModularArm
(notebook) while the Arm ABC remains untouched, so the test suite stays green.
This is a prerequisite for a later change that declares both methods as
abstract on the Arm ABC.
FakeArmClient in tests/test_robot.py is a sixth Arm subclass that the
original Task 1 file list missed. Without these methods it fails to
instantiate once move_through_joint_positions and get_3d_models become
abstract on the Arm ABC.
my_cool_arm.py and complex_module's my_arm.py accepted MoveOptions in
move_through_joint_positions but silently ignored it, with the only comment
pointing at cancellation instead. A module author copying these examples
would ship a driver that accepts speed/accel limits and drops them on the
floor. Also fixes FakeArmClient in test_robot.py to pass options by keyword,
matching extra/timeout, so a future keyword-only change on ArmClient doesn't
break it at a distance.
…, and client

Declares the method as abstract on the Arm ABC and implements it in
ArmRPCService and ArmClient in the same change, since ArmClient is a
seventh Arm subclass (beyond the six covered by the prior mock/example
commit) and would otherwise fail to instantiate once the ABC method is
abstract. The service guards on MoveOptions field presence so "no
limit requested" (None) is never conflated with a zeroed MoveOptions,
which a driver could misread as "do not move".
…ence

Address code review on move_through_joint_positions:
- ArmRPCService now calls the resource's move_through_joint_positions with
  options=options (keyword) instead of positionally, so drivers that
  reasonably declare it keyword-only don't blow up at runtime.
- Extend the ABC's Note to cover the same 0.0-vs-unset ambiguity one level
  deeper: every scalar on MoveOptions has its own explicit presence, so
  reading e.g. max_vel_degs_per_sec without a HasField check silently
  reads "unset" as "stop". Also document the proto's precedence rule
  (per-joint limits override the scalar ceiling) and that an empty
  positions list must be handled by implementations.
- Update the two example drivers' comments to point at HasField instead of
  naming the scalar directly, so they don't teach the wrong pattern.
- Add the MoveOptions import hint to the Arm class docstring, and move the
  ABC's move_through_joint_positions to sit after move_to_joint_positions,
  matching the order already used in client.py, service.py, and MockArm.
- Cover the keyword form (options=..., timeout=...) in TestClient, moving
  the new timeout assertion onto the test that runs first so the
  timeout-is-None invariant TestClient.test_stop relies on still holds.
Address code review on get_3d_models:
- ArmClient.get_3d_models now copies the protobuf map response into a
  plain dict before returning it. Protobuf map containers create and
  insert a default-constructed value on __getitem__ for an absent key
  instead of raising KeyError, silently violating the Mapping[str, Mesh]
  contract and corrupting subsequent len()/iteration on the caller's
  reference. get_kinematics has the same wart on its third element but
  is left alone as pre-existing and out of scope.
- Cross-reference the get_3d_models and get_kinematics docstrings so
  readers of either know the two are keyed differently (by model name
  vs. by URDF filepath) and aren't the same data.
- Reword "an arm with no models returns an empty mapping" as an
  implementer obligation ("must return an empty mapping, not None"),
  matching the Note: style used in move_through_joint_positions.
- Unify the Arm class docstring's import guidance to the package
  re-export form for all four types (Pose, JointPositions, MoveOptions,
  Mesh), since all four are in __all__.
- Soften docs/examples/my_cool_arm.py's comment from "keyed by link
  name" to "keyed by name" to match the ABC and avoid over-claiming
  beyond what the proto documents.
- Move ArmRPCService.Get3DModels to sit between GetKinematics and
  GetGeometries, matching ArmClient's method ordering.
- Tighten TestClient.test_get_3d_models to assert equality against the
  live Mapping (not a dict() copy, which would pass either way) and
  add a KeyError assertion plus a timeout assertion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RbNNgUjePEdPuUGAdU3qb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants