From 219747fe6642b1aada47098326cc6f5ad0cb447f Mon Sep 17 00:00:00 2001 From: Eliza Farley Date: Tue, 15 Sep 2026 10:51:45 -0400 Subject: [PATCH 1/3] DOCS-4801: Document arm MoveThroughJointPositionsStreamed for Go and C++ api#874 added the MoveThroughJointPositionsStreamed bidi-streaming RPC to the Arm API. rdk#6192 and viam-cpp-sdk#675 implement it in the Go and C++ SDKs; the universal-robots and ufactory-xarm modules both support it. The Python and TypeScript SDKs do not expose it yet, so neither is documented here. Adds a MoveThroughJointPositionsStreamed section to the joint-positions guide with Go and C++ examples, the unit split across the three interfaces, and two troubleshooting entries. Adds the method to the generated arm API reference. The generator crashed on its Go signature: parse_method_usage() only handled the bare `ch chan Tick` shape, so directional channel params fell through every branch and left type_link unbound. Handle them and keep the direction arrow HTML-escaped so the rendered link text shows it. Co-Authored-By: Claude Opus 5 --- .github/workflows/sdk_protos_map.csv | 1 + .github/workflows/update_sdk_methods.py | 11 + .../move-an-arm/move-by-joint-positions.md | 212 +++++++++++++++++- .../components/apis/generated/arm-table.md | 1 + .../include/components/apis/generated/arm.md | 24 ++ ...veThroughJointPositionsStreamed.batches.md | 1 + ...ThroughJointPositionsStreamed.responses.md | 1 + .../arm.MoveThroughJointPositionsStreamed.md | 2 + 8 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.batches.md create mode 100644 static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.responses.md create mode 100644 static/include/components/apis/overrides/protos/arm.MoveThroughJointPositionsStreamed.md diff --git a/.github/workflows/sdk_protos_map.csv b/.github/workflows/sdk_protos_map.csv index 138081ad7b..a3a10f30f5 100644 --- a/.github/workflows/sdk_protos_map.csv +++ b/.github/workflows/sdk_protos_map.csv @@ -5,6 +5,7 @@ arm,GetEndPosition,,get_end_position,EndPosition,endPosition,getEndPosition arm,MoveToPosition,,move_to_position,MoveToPosition,moveToPosition,moveToPosition arm,MoveToJointPositions,,move_to_joint_positions,MoveToJointPositions,moveToJointPositions,moveToJointPositions arm,MoveThroughJointPositions,,,MoveThroughJointPositions,, +arm,MoveThroughJointPositionsStreamed,,,MoveThroughJointPositionsStreamed,, arm,GetJointPositions,,get_joint_positions,JointPositions,jointPositions,getJointPositions arm,Get3DModels,,,Get3DModels,get3DModels,get3DModels ## Flutter-only client-side helper, sums link lengths from getKinematics() locally; no proto/RPC and no analog in other SDKs: diff --git a/.github/workflows/update_sdk_methods.py b/.github/workflows/update_sdk_methods.py index a5c77ed099..43eefe1587 100755 --- a/.github/workflows/update_sdk_methods.py +++ b/.github/workflows/update_sdk_methods.py @@ -543,6 +543,17 @@ def parse_method_usage(usage_string): type_name = 'ch chan' param_type = 'Tick' type_link = '#Tick' + ## Handle directional channel params, such as Arm > MoveThroughJointPositionsStreamed's + ## 'batches <-chan []TrajectoryPoint'. The direction arrow arrives HTML-escaped and is + ## left that way so the rendered link text shows the arrow instead of swallowing it: + elif len(param_raw) == 3 and 'chan' in param_raw[1]: + type_name = param_raw[0] + param_type = param_raw[1] + ' ' + param_raw[2] + try: + type_link = regex.findall(r'href="([^"]+)">', param)[-1] + except: + print("DEBUG: No type link found: {}, {}".format(usage_string, param)) + type_link = None ## Handle named parameters: elif len(param_raw) == 2: type_name = param_raw[0] diff --git a/docs/motion-planning/move-an-arm/move-by-joint-positions.md b/docs/motion-planning/move-an-arm/move-by-joint-positions.md index 250a97d57a..9b171b9c44 100644 --- a/docs/motion-planning/move-an-arm/move-by-joint-positions.md +++ b/docs/motion-planning/move-an-arm/move-by-joint-positions.md @@ -4,7 +4,7 @@ title: "Move an arm by setting joint positions" weight: 30 layout: "docs" type: "docs" -description: "Command an arm directly in joint space using MoveToJointPositions and MoveThroughJointPositions, bypassing the motion planner." +description: "Command an arm directly in joint space using MoveToJointPositions, MoveThroughJointPositions, and MoveThroughJointPositionsStreamed, bypassing the motion planner." capabilities: ["motion-planning", "hw-arm"] aliases: - /motion-planning/motion-how-to/move-arm-joint-positions/ @@ -193,6 +193,175 @@ values in **radians**: `MaxVelRads`, `MaxAccRads`, `MaxVelRadsJoints`, `MaxAccRadsJoints`, `MaxTCPSpeedMPerSec`. The conversion happens at the wire boundary. +## MoveThroughJointPositionsStreamed + +`MoveThroughJointPositions` needs the whole trajectory before the arm starts +moving. `MoveThroughJointPositionsStreamed` does not: you open a stream, push +batches of waypoints onto it, and the arm executes the points it already has +while you keep appending. Reach for it when the trajectory is produced as the +motion runs: a teleoperation feed, a visual-servoing loop, or a trajectory long +enough that you do not want to hold all of it in memory. + +Each waypoint is a `TrajectoryPoint`: a time offset from the start of the +motion, a joint configuration to be at by then, and optional velocity and +acceleration targets. The time of the first point must be zero, and times must +strictly increase across the whole stream, not just within a batch. If a point +carries constraints, the velocities on the t=0 point must all be zero. + +Batching is purely your pacing choice. Points execute in the order you send +them regardless of how you group them, so a batch is just how much you hand +over at once. + +{{< alert title="SDK availability" color="caution" >}} +`MoveThroughJointPositionsStreamed` is available in the **Go SDK** and the +**C++ SDK**. The Python and TypeScript SDKs do not expose it yet. +{{< /alert >}} + +{{< tabs >}} +{{% tab name="Go" %}} + +The call blocks until the trajectory finishes or fails. You own both channels: +send batches on `batches` and close it when the trajectory is complete, read +acknowledgments off `responses` for the life of the call, and close `responses` +only after the call returns. + +```go +import ( + "math" + "time" + + "go.viam.com/rdk/components/arm" + "go.viam.com/rdk/referenceframe" +) + +// One batch of three waypoints. Times are offsets from the start of the +// motion; positions are radians, matching referenceframe.Input. +firstBatch := []arm.TrajectoryPoint{ + { + Time: 0, + Positions: []referenceframe.Input{0, -math.Pi / 4, math.Pi / 2, 0, math.Pi / 4, 0}, + // Velocities on the t=0 point must be zero. + Constraints: &arm.KinematicConstraints{ + Velocities: []float64{0, 0, 0, 0, 0, 0}, + }, + }, + { + Time: 500 * time.Millisecond, + Positions: []referenceframe.Input{0, -math.Pi / 8, math.Pi / 2, 0, math.Pi / 8, 0}, + }, + { + Time: time.Second, + Positions: []referenceframe.Input{0, 0, math.Pi / 2, 0, 0, 0}, + }, +} + +batches := make(chan []arm.TrajectoryPoint) +responses := make(chan arm.Response) + +// Drain acknowledgments. The arm is not obliged to acknowledge every batch, +// but a caller that stops reading stalls the stream. +go func() { + for range responses { + } +}() + +// Feed the trajectory, then close to signal that no more points are coming. +go func() { + defer close(batches) + for _, batch := range [][]arm.TrajectoryPoint{firstBatch /*, more batches */} { + select { + case batches <- batch: + case <-ctx.Done(): + return + } + } +}() + +err := myArm.MoveThroughJointPositionsStreamed(ctx, batches, responses, nil) +close(responses) +if err != nil { + logger.Fatal(err) +} +``` + +{{% /tab %}} +{{% tab name="C++" %}} + +The C++ SDK inverts the control flow: instead of you pushing onto a channel, +the SDK pulls from a `batch_source` callback until it returns `boost::none`, +and reports progress through an `update_handler` callback. Returning `false` +from `update_handler` stops the trajectory early. + +```cpp +#include + +using viam::sdk::Arm; + +std::vector> trajectory = { + { + // Positions and velocities are in degrees, unlike the Go SDK. + // Velocities on the t=0 point must be zero. + Arm::trajectory_point{std::chrono::microseconds(0), + {0, -45, 90, 0, 45, 0}, + Arm::trajectory_point::kinematic_constraints{{0, 0, 0, 0, 0, 0}, + boost::none}}, + Arm::trajectory_point{std::chrono::milliseconds(500), {0, -22.5, 90, 0, 22.5, 0}, boost::none}, + Arm::trajectory_point{std::chrono::seconds(1), {0, 0, 90, 0, 0, 0}, boost::none}, + }, +}; + +std::size_t next = 0; +auto batch_source = [&]() -> boost::optional> { + if (next == trajectory.size()) { + return boost::none; // No more points are coming. + } + return trajectory[next++]; +}; + +// Return false here to halt the trajectory early. +auto update_handler = [](Arm::trajectory_update) { return true; }; + +const auto outcome = my_arm->move_through_joint_positions_streamed(batch_source, update_handler); +if (outcome == Arm::stream_outcome::k_halted_by_update_handler) { + // The trajectory was stopped before its natural end. +} +``` + +The two callbacks may be invoked from different threads and may run +concurrently with each other, so synchronize any state you share between them. +A fault is reported by throwing, and an exception thrown out of either callback +propagates to the caller rather than being swallowed. + +{{% /tab %}} +{{< /tabs >}} + +### Units, again + +The unit split from `MoveToJointPositions` carries over, and the C++ SDK adds a +third position: + +| Interface | Positions | Velocities | +| --------------------------- | -------------------------------- | ---------------------------------- | +| Proto wire format | degrees, millimeters | degrees/second, millimeters/second | +| Go `arm.TrajectoryPoint` | radians (`referenceframe.Input`) | radians/second | +| C++ `Arm::trajectory_point` | degrees | degrees/second | + +Accelerations follow their velocity unit, squared. + +### What the SDK checks before the wire + +The Go client validates each waypoint against the arm's joint limits as it +encodes it, the same check the unary path makes, advancing through the +trajectory point by point. Because batches are already in flight by the time a +bad waypoint appears, a rejected waypoint tears the whole stream down rather +than returning an error for that point alone. If the arm's kinematics are not +registered, the client logs a warning and skips the check. + +`MoveThroughJointPositionsStreamed` is safety-heartbeat monitored: if the +session that last called it stops sending heartbeats, the arm is stopped. A +client that dies mid-trajectory does not leave the arm executing the rest of +what it was sent. + ## Reading current joint positions Use `GetJointPositions` to capture the arm's current configuration @@ -226,12 +395,13 @@ programmatically. ## Joint-space moves compared to motion.Move -| Motion path | Use when | -| ------------------------------------ | --------------------------------------------------------------------------------------------------- | -| `arm.MoveToJointPositions` | You know the joint angles you want. | -| `arm.MoveThroughJointPositions` (Go) | You have a sequence of joint targets and want per-call velocity or acceleration caps. | -| `arm.MoveToPosition` | You have a Cartesian target pose but don't need obstacle avoidance. | -| `motion.Move` | You have a Cartesian target and want obstacle avoidance, constraints, and IK picked by the planner. | +| Motion path | Use when | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| `arm.MoveToJointPositions` | You know the joint angles you want. | +| `arm.MoveThroughJointPositions` (Go) | You have a sequence of joint targets and want per-call velocity or acceleration caps. | +| `arm.MoveThroughJointPositionsStreamed` (Go, C++) | You are producing the trajectory as the arm moves and cannot supply it all up front. | +| `arm.MoveToPosition` | You have a Cartesian target pose but don't need obstacle avoidance. | +| `motion.Move` | You have a Cartesian target and want obstacle avoidance, constraints, and IK picked by the planner. | Joint-space moves are the right call when you need to control the posture of the arm precisely. They do not protect against collisions @@ -261,6 +431,34 @@ shorter `MoveToJointPositions` calls with sleeps between. {{< /expand >}} +{{< expand "Streamed trajectory behaves oddly across a batch boundary" >}} + +Times are offsets from the start of the whole motion, not from the start of the +batch they arrive in. The first point of the stream must be at time zero and +every later point must be strictly greater than the one before it, across batch +boundaries as well as within a batch. Restarting the clock at each batch sends +the arm a trajectory that goes backwards in time. + +`viam-server` does not check this for you. Enforcement is left to the arm +module, so what a violation looks like depends on the module: an error, a +refused batch, or motion you did not intend. + +{{< /expand >}} + +{{< expand "Streamed trajectory fails with a joint range error" >}} + +The Go client checks each waypoint against the arm's joint limits as it encodes +it, and refuses one that is out of range. Earlier batches are already in flight +by then, so a rejected waypoint tears the whole stream down instead of failing +just that point. + +The error names the joint index and the range it violated, not which waypoint +carried it: `joint 1 needs to be within range [-360, 360] and cannot be moved +to 400`. Check the whole trajectory against the joint limits before you start +streaming if you need to know which point is at fault. + +{{< /expand >}} + {{< expand "Wrong number of values error" >}} The `values` array must match the arm's degrees of freedom. A 6-DOF diff --git a/static/include/components/apis/generated/arm-table.md b/static/include/components/apis/generated/arm-table.md index 8975707249..ee74129d4d 100644 --- a/static/include/components/apis/generated/arm-table.md +++ b/static/include/components/apis/generated/arm-table.md @@ -5,6 +5,7 @@ | [`MoveToPosition`](/reference/apis/components/arm/#movetoposition) | Move the end of the arm in a straight line to the desired pose, relative to the base of the arm. | | [`MoveToJointPositions`](/reference/apis/components/arm/#movetojointpositions) | Move each joint on the arm to the position specified in `positions`. | | [`MoveThroughJointPositions`](/reference/apis/components/arm/#movethroughjointpositions) | Move the arm's joints through the given positions in the order they are specified. | +| [`MoveThroughJointPositionsStreamed`](/reference/apis/components/arm/#movethroughjointpositionsstreamed) | Stream batches of timed trajectory points to the arm and execute them in order as they arrive. | | [`GetJointPositions`](/reference/apis/components/arm/#getjointpositions) | Get the current position of each joint on the arm. | | [`Get3DModels`](/reference/apis/components/arm/#get3dmodels) | Get the 3D models of the arm. | | [`CalculateMaxReach`](/reference/apis/components/arm/#calculatemaxreach) | Calculate the maximum reach of the arm by summing all link lengths from its kinematics data. | diff --git a/static/include/components/apis/generated/arm.md b/static/include/components/apis/generated/arm.md index 8f3d2eb425..ea2b83a4af 100644 --- a/static/include/components/apis/generated/arm.md +++ b/static/include/components/apis/generated/arm.md @@ -376,6 +376,30 @@ For more information, see the [Go SDK Docs](https://pkg.go.dev/go.viam.com/rdk/c {{% /tab %}} {{< /tabs >}} +### MoveThroughJointPositionsStreamed + +Stream batches of timed trajectory points to the arm and execute them in order as they arrive. +Unlike `MoveThroughJointPositions`, the full trajectory does not have to be known before the motion starts: the caller keeps appending points while the arm executes the ones it already has. + +{{< tabs >}} +{{% tab name="Go" %}} + +**Parameters:** + +- `ctx` [(Context)](https://pkg.go.dev/context#Context): A Context carries a deadline, a cancellation signal, and other values across API boundaries. +- `batches` [(<-chan []TrajectoryPoint)](https://pkg.go.dev/go.viam.com/rdk/components/arm#TrajectoryPoint): The channel the caller sends trajectory batches on. Each send is one batch of `TrajectoryPoint` values, appended to the motion in order. Close the channel to signal that no more points are coming. +- `responses` [(chan<- Response)](https://pkg.go.dev/go.viam.com/rdk/components/arm#Response): The channel acknowledgments arrive on while the arm executes. `Response` carries no fields today. The caller must drain this channel for the duration of the call, and closes it only after the call returns. +- `extra` [(map[string]interface{})](https://go.dev/blog/maps): Extra options to pass to the underlying RPC call. + +**Returns:** + +- [(error)](https://pkg.go.dev/builtin#error): An error, if one occurred. + +For more information, see the [Go SDK Docs](https://pkg.go.dev/go.viam.com/rdk/components/arm#Arm). + +{{% /tab %}} +{{< /tabs >}} + ### GetJointPositions Get the current position of each joint on the arm. diff --git a/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.batches.md b/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.batches.md new file mode 100644 index 0000000000..9ff312bfbc --- /dev/null +++ b/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.batches.md @@ -0,0 +1 @@ +The channel the caller sends trajectory batches on. Each send is one batch of `TrajectoryPoint` values, appended to the motion in order. Close the channel to signal that no more points are coming. diff --git a/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.responses.md b/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.responses.md new file mode 100644 index 0000000000..afb4746122 --- /dev/null +++ b/static/include/components/apis/overrides/methods/go.arm.MoveThroughJointPositionsStreamed.responses.md @@ -0,0 +1 @@ +The channel acknowledgments arrive on while the arm executes. `Response` carries no fields today. The caller must drain this channel for the duration of the call, and closes it only after the call returns. diff --git a/static/include/components/apis/overrides/protos/arm.MoveThroughJointPositionsStreamed.md b/static/include/components/apis/overrides/protos/arm.MoveThroughJointPositionsStreamed.md new file mode 100644 index 0000000000..6d4f502959 --- /dev/null +++ b/static/include/components/apis/overrides/protos/arm.MoveThroughJointPositionsStreamed.md @@ -0,0 +1,2 @@ +Stream batches of timed trajectory points to the arm and execute them in order as they arrive. +Unlike `MoveThroughJointPositions`, the full trajectory does not have to be known before the motion starts: the caller keeps appending points while the arm executes the ones it already has. From 23adfc3e64dcf515cb21570f6388dca2c2a34f18 Mon Sep 17 00:00:00 2001 From: Eliza Farley Date: Wed, 16 Sep 2026 16:34:46 -0400 Subject: [PATCH 2/3] DOCS-4801: Add Python SDK support for arm MoveThroughJointPositionsStreamed viam-python-sdk#1242 merged, adding Python client support for the streamed arm RPC. Splits MoveThroughJointPositionsStreamed out of move-by-joint-positions.md into its own page now that three SDKs cover it, and fills in comparison-table rows on overview.md that were missing for existing pages. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/sdk_protos_map.csv | 2 +- .../move-an-arm/move-by-joint-positions.md | 220 +--------------- docs/motion-planning/move-an-arm/overview.md | 23 +- .../move-an-arm/stream-joint-positions.md | 244 ++++++++++++++++++ .../include/components/apis/generated/arm.md | 31 +++ 5 files changed, 304 insertions(+), 216 deletions(-) create mode 100644 docs/motion-planning/move-an-arm/stream-joint-positions.md diff --git a/.github/workflows/sdk_protos_map.csv b/.github/workflows/sdk_protos_map.csv index a3a10f30f5..115db7144a 100644 --- a/.github/workflows/sdk_protos_map.csv +++ b/.github/workflows/sdk_protos_map.csv @@ -5,7 +5,7 @@ arm,GetEndPosition,,get_end_position,EndPosition,endPosition,getEndPosition arm,MoveToPosition,,move_to_position,MoveToPosition,moveToPosition,moveToPosition arm,MoveToJointPositions,,move_to_joint_positions,MoveToJointPositions,moveToJointPositions,moveToJointPositions arm,MoveThroughJointPositions,,,MoveThroughJointPositions,, -arm,MoveThroughJointPositionsStreamed,,,MoveThroughJointPositionsStreamed,, +arm,MoveThroughJointPositionsStreamed,,move_through_joint_positions_streamed,MoveThroughJointPositionsStreamed,, arm,GetJointPositions,,get_joint_positions,JointPositions,jointPositions,getJointPositions arm,Get3DModels,,,Get3DModels,get3DModels,get3DModels ## Flutter-only client-side helper, sums link lengths from getKinematics() locally; no proto/RPC and no analog in other SDKs: diff --git a/docs/motion-planning/move-an-arm/move-by-joint-positions.md b/docs/motion-planning/move-an-arm/move-by-joint-positions.md index 9b171b9c44..e830ac584e 100644 --- a/docs/motion-planning/move-an-arm/move-by-joint-positions.md +++ b/docs/motion-planning/move-an-arm/move-by-joint-positions.md @@ -4,7 +4,7 @@ title: "Move an arm by setting joint positions" weight: 30 layout: "docs" type: "docs" -description: "Command an arm directly in joint space using MoveToJointPositions, MoveThroughJointPositions, and MoveThroughJointPositionsStreamed, bypassing the motion planner." +description: "Command an arm directly in joint space using MoveToJointPositions and MoveThroughJointPositions, bypassing the motion planner." capabilities: ["motion-planning", "hw-arm"] aliases: - /motion-planning/motion-how-to/move-arm-joint-positions/ @@ -21,7 +21,10 @@ are different tools. You reach for joint-space when: causes a wrist flip or elbow reconfiguration. - You want predictable motion between two configurations you both control. -- You are building a control loop that computes its own joint targets. + +Both methods on this page need every waypoint before the arm starts moving. If +you are computing the trajectory as the arm runs, see +[Stream joint positions to an arm](/motion-planning/move-an-arm/stream-joint-positions/). **A caveat before you dive in.** Joint-space moves bypass the motion planner. No obstacle avoidance, no constraint satisfaction, no path smoothing. If the @@ -193,175 +196,6 @@ values in **radians**: `MaxVelRads`, `MaxAccRads`, `MaxVelRadsJoints`, `MaxAccRadsJoints`, `MaxTCPSpeedMPerSec`. The conversion happens at the wire boundary. -## MoveThroughJointPositionsStreamed - -`MoveThroughJointPositions` needs the whole trajectory before the arm starts -moving. `MoveThroughJointPositionsStreamed` does not: you open a stream, push -batches of waypoints onto it, and the arm executes the points it already has -while you keep appending. Reach for it when the trajectory is produced as the -motion runs: a teleoperation feed, a visual-servoing loop, or a trajectory long -enough that you do not want to hold all of it in memory. - -Each waypoint is a `TrajectoryPoint`: a time offset from the start of the -motion, a joint configuration to be at by then, and optional velocity and -acceleration targets. The time of the first point must be zero, and times must -strictly increase across the whole stream, not just within a batch. If a point -carries constraints, the velocities on the t=0 point must all be zero. - -Batching is purely your pacing choice. Points execute in the order you send -them regardless of how you group them, so a batch is just how much you hand -over at once. - -{{< alert title="SDK availability" color="caution" >}} -`MoveThroughJointPositionsStreamed` is available in the **Go SDK** and the -**C++ SDK**. The Python and TypeScript SDKs do not expose it yet. -{{< /alert >}} - -{{< tabs >}} -{{% tab name="Go" %}} - -The call blocks until the trajectory finishes or fails. You own both channels: -send batches on `batches` and close it when the trajectory is complete, read -acknowledgments off `responses` for the life of the call, and close `responses` -only after the call returns. - -```go -import ( - "math" - "time" - - "go.viam.com/rdk/components/arm" - "go.viam.com/rdk/referenceframe" -) - -// One batch of three waypoints. Times are offsets from the start of the -// motion; positions are radians, matching referenceframe.Input. -firstBatch := []arm.TrajectoryPoint{ - { - Time: 0, - Positions: []referenceframe.Input{0, -math.Pi / 4, math.Pi / 2, 0, math.Pi / 4, 0}, - // Velocities on the t=0 point must be zero. - Constraints: &arm.KinematicConstraints{ - Velocities: []float64{0, 0, 0, 0, 0, 0}, - }, - }, - { - Time: 500 * time.Millisecond, - Positions: []referenceframe.Input{0, -math.Pi / 8, math.Pi / 2, 0, math.Pi / 8, 0}, - }, - { - Time: time.Second, - Positions: []referenceframe.Input{0, 0, math.Pi / 2, 0, 0, 0}, - }, -} - -batches := make(chan []arm.TrajectoryPoint) -responses := make(chan arm.Response) - -// Drain acknowledgments. The arm is not obliged to acknowledge every batch, -// but a caller that stops reading stalls the stream. -go func() { - for range responses { - } -}() - -// Feed the trajectory, then close to signal that no more points are coming. -go func() { - defer close(batches) - for _, batch := range [][]arm.TrajectoryPoint{firstBatch /*, more batches */} { - select { - case batches <- batch: - case <-ctx.Done(): - return - } - } -}() - -err := myArm.MoveThroughJointPositionsStreamed(ctx, batches, responses, nil) -close(responses) -if err != nil { - logger.Fatal(err) -} -``` - -{{% /tab %}} -{{% tab name="C++" %}} - -The C++ SDK inverts the control flow: instead of you pushing onto a channel, -the SDK pulls from a `batch_source` callback until it returns `boost::none`, -and reports progress through an `update_handler` callback. Returning `false` -from `update_handler` stops the trajectory early. - -```cpp -#include - -using viam::sdk::Arm; - -std::vector> trajectory = { - { - // Positions and velocities are in degrees, unlike the Go SDK. - // Velocities on the t=0 point must be zero. - Arm::trajectory_point{std::chrono::microseconds(0), - {0, -45, 90, 0, 45, 0}, - Arm::trajectory_point::kinematic_constraints{{0, 0, 0, 0, 0, 0}, - boost::none}}, - Arm::trajectory_point{std::chrono::milliseconds(500), {0, -22.5, 90, 0, 22.5, 0}, boost::none}, - Arm::trajectory_point{std::chrono::seconds(1), {0, 0, 90, 0, 0, 0}, boost::none}, - }, -}; - -std::size_t next = 0; -auto batch_source = [&]() -> boost::optional> { - if (next == trajectory.size()) { - return boost::none; // No more points are coming. - } - return trajectory[next++]; -}; - -// Return false here to halt the trajectory early. -auto update_handler = [](Arm::trajectory_update) { return true; }; - -const auto outcome = my_arm->move_through_joint_positions_streamed(batch_source, update_handler); -if (outcome == Arm::stream_outcome::k_halted_by_update_handler) { - // The trajectory was stopped before its natural end. -} -``` - -The two callbacks may be invoked from different threads and may run -concurrently with each other, so synchronize any state you share between them. -A fault is reported by throwing, and an exception thrown out of either callback -propagates to the caller rather than being swallowed. - -{{% /tab %}} -{{< /tabs >}} - -### Units, again - -The unit split from `MoveToJointPositions` carries over, and the C++ SDK adds a -third position: - -| Interface | Positions | Velocities | -| --------------------------- | -------------------------------- | ---------------------------------- | -| Proto wire format | degrees, millimeters | degrees/second, millimeters/second | -| Go `arm.TrajectoryPoint` | radians (`referenceframe.Input`) | radians/second | -| C++ `Arm::trajectory_point` | degrees | degrees/second | - -Accelerations follow their velocity unit, squared. - -### What the SDK checks before the wire - -The Go client validates each waypoint against the arm's joint limits as it -encodes it, the same check the unary path makes, advancing through the -trajectory point by point. Because batches are already in flight by the time a -bad waypoint appears, a rejected waypoint tears the whole stream down rather -than returning an error for that point alone. If the arm's kinematics are not -registered, the client logs a warning and skips the check. - -`MoveThroughJointPositionsStreamed` is safety-heartbeat monitored: if the -session that last called it stops sending heartbeats, the arm is stopped. A -client that dies mid-trajectory does not leave the arm executing the rest of -what it was sent. - ## Reading current joint positions Use `GetJointPositions` to capture the arm's current configuration @@ -395,13 +229,13 @@ programmatically. ## Joint-space moves compared to motion.Move -| Motion path | Use when | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `arm.MoveToJointPositions` | You know the joint angles you want. | -| `arm.MoveThroughJointPositions` (Go) | You have a sequence of joint targets and want per-call velocity or acceleration caps. | -| `arm.MoveThroughJointPositionsStreamed` (Go, C++) | You are producing the trajectory as the arm moves and cannot supply it all up front. | -| `arm.MoveToPosition` | You have a Cartesian target pose but don't need obstacle avoidance. | -| `motion.Move` | You have a Cartesian target and want obstacle avoidance, constraints, and IK picked by the planner. | +| Motion path | Use when | +| ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| `arm.MoveToJointPositions` | You know the joint angles you want. | +| `arm.MoveThroughJointPositions` (Go) | You have a sequence of joint targets and want per-call velocity or acceleration caps. | +| [`arm.MoveThroughJointPositionsStreamed`](/motion-planning/move-an-arm/stream-joint-positions/) (Python, Go, C++) | You are producing the trajectory as the arm moves and cannot supply it all up front. | +| `arm.MoveToPosition` | You have a Cartesian target pose but don't need obstacle avoidance. | +| `motion.Move` | You have a Cartesian target and want obstacle avoidance, constraints, and IK picked by the planner. | Joint-space moves are the right call when you need to control the posture of the arm precisely. They do not protect against collisions @@ -431,34 +265,6 @@ shorter `MoveToJointPositions` calls with sleeps between. {{< /expand >}} -{{< expand "Streamed trajectory behaves oddly across a batch boundary" >}} - -Times are offsets from the start of the whole motion, not from the start of the -batch they arrive in. The first point of the stream must be at time zero and -every later point must be strictly greater than the one before it, across batch -boundaries as well as within a batch. Restarting the clock at each batch sends -the arm a trajectory that goes backwards in time. - -`viam-server` does not check this for you. Enforcement is left to the arm -module, so what a violation looks like depends on the module: an error, a -refused batch, or motion you did not intend. - -{{< /expand >}} - -{{< expand "Streamed trajectory fails with a joint range error" >}} - -The Go client checks each waypoint against the arm's joint limits as it encodes -it, and refuses one that is out of range. Earlier batches are already in flight -by then, so a rejected waypoint tears the whole stream down instead of failing -just that point. - -The error names the joint index and the range it violated, not which waypoint -carried it: `joint 1 needs to be within range [-360, 360] and cannot be moved -to 400`. Check the whole trajectory against the joint limits before you start -streaming if you need to know which point is at fault. - -{{< /expand >}} - {{< expand "Wrong number of values error" >}} The `values` array must match the arm's degrees of freedom. A 6-DOF @@ -469,6 +275,8 @@ module's documentation or the kinematics file. ## What's next +- [Stream joint positions to an arm](/motion-planning/move-an-arm/stream-joint-positions/): + push waypoints while the arm is already moving. - [Move an arm to a pose](/motion-planning/move-an-arm/move-to-pose/): Cartesian motion with obstacle avoidance through `motion.Move`. - [Move with constraints](/motion-planning/move-an-arm/move-with-constraints/): diff --git a/docs/motion-planning/move-an-arm/overview.md b/docs/motion-planning/move-an-arm/overview.md index 0731618723..99315ad292 100644 --- a/docs/motion-planning/move-an-arm/overview.md +++ b/docs/motion-planning/move-an-arm/overview.md @@ -10,24 +10,28 @@ aliases: - /motion-planning/pick-and-place/ --- -Viam exposes three ways to command an arm. Three questions sort them: +Viam exposes multiple ways to command an arm. Three questions sort them: 1. **What do you know about the destination?** A Cartesian target (a pose - in space) calls for the motion service. A specific joint configuration - calls for direct joint commands. + in space), a region of acceptable poses, an ordered list of goals, or a + specific joint configuration each point to a different call. 2. **Does the path matter, or only the endpoint?** If you need a straight line, a fixed orientation, or any other rule about the path itself, you need constraints. -3. **Do you want obstacle avoidance and IK picked for you, or fine +3. **Do you want obstacle avoidance and inverse kinematics (IK) picked for you, or fine manual control?** The motion service picks the IK solution and plans around obstacles for you; direct joint commands execute exactly the angles you send. -| Pattern | Input | Obstacle avoidance | Path-shape control | When to pick | -| -------------------------------------------------------------------------------- | ------------------------ | ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| [Move to a pose](/motion-planning/move-an-arm/move-to-pose/) | Cartesian target | Yes | No | You know where the end effector needs to go and want the planner to choose the path. | -| [Move with constraints](/motion-planning/move-an-arm/move-with-constraints/) | Cartesian target + rules | Yes | Yes | The shape of the motion matters (straight-line tool path, level end effector). | -| [Move by joint positions](/motion-planning/move-an-arm/move-by-joint-positions/) | Joint angles | No | Direct | You know the joint angles, need predictable motion between known configurations, or want to avoid the planner picking an unexpected IK solution. | +| Pattern | Input | Obstacle avoidance | Path-shape control | When to pick | +| -------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| [Move to a pose](/motion-planning/move-an-arm/move-to-pose/) | Cartesian target | Yes | No | You know where the end effector needs to go and want the planner to choose the path. | +| [Move with constraints](/motion-planning/move-an-arm/move-with-constraints/) | Cartesian target + rules | Yes | Yes | The shape of the motion matters (straight-line tool path, level end effector). | +| [Relax a goal with a pose cloud](/motion-planning/move-an-arm/pose-clouds/) | Cartesian target + tolerances | Yes | No | Any pose in a region will do, or an exact goal plans slowly or keeps failing. | +| [Move through waypoints](/motion-planning/move-an-arm/multiple-waypoints/) | Ordered list of Cartesian and/or joint goals | Yes | Partial | The arm must pass through specific intermediate goals in one continuous trajectory. | +| [Move by joint positions](/motion-planning/move-an-arm/move-by-joint-positions/) | Joint angles | No | Direct | You know the joint angles, need predictable motion between known configurations, or want to avoid the planner picking an unexpected IK solution. | +| [Stream joint positions](/motion-planning/move-an-arm/stream-joint-positions/) | Joint waypoints with times, sent while the arm moves | No | Direct | You are computing the trajectory as the motion runs, from a teleoperation feed or a control loop. | +| [Arm-level Cartesian move](/reference/apis/components/arm/#movetoposition) | Cartesian target | No | No | You have a pose and deliberately want the arm's own IK, with no planner, frame system, or obstacle checking. | For the four constraint types the planner enforces, see [Configure motion constraints](/motion-planning/move-an-arm/constraints/). @@ -59,6 +63,7 @@ from the table above. {{% card link="/motion-planning/move-an-arm/move-with-constraints/" noimage="true" %}} {{% card link="/motion-planning/move-an-arm/constraints/" noimage="true" %}} {{% card link="/motion-planning/move-an-arm/move-by-joint-positions/" noimage="true" %}} +{{% card link="/motion-planning/move-an-arm/stream-joint-positions/" noimage="true" %}} {{% card link="/motion-planning/move-an-arm/multiple-waypoints/" noimage="true" %}} {{% card link="/motion-planning/move-an-arm/pose-clouds/" noimage="true" %}} {{% card link="/motion-planning/move-an-arm/pick-an-object/" noimage="true" %}} diff --git a/docs/motion-planning/move-an-arm/stream-joint-positions.md b/docs/motion-planning/move-an-arm/stream-joint-positions.md new file mode 100644 index 0000000000..ea57dd7e5d --- /dev/null +++ b/docs/motion-planning/move-an-arm/stream-joint-positions.md @@ -0,0 +1,244 @@ +--- +linkTitle: "Stream joint positions" +title: "Stream joint positions to an arm" +weight: 35 +layout: "docs" +type: "docs" +description: "Push joint trajectory points to an arm while it is already moving, using MoveThroughJointPositionsStreamed." +capabilities: ["motion-planning", "hw-arm"] +--- + +`MoveThroughJointPositionsStreamed` opens a stream onto which you push joint +trajectory points while the arm is already moving: the arm executes the points +it has while you keep appending. The +[other joint-space methods](/motion-planning/move-an-arm/move-by-joint-positions/) +take the whole motion up front, the configurations to hit and optional +ceilings on how fast to get there. The arm module picks the motion profile from +there. + +Streaming hands the arm a time-parameterized trajectory: be at this +configuration at this time, moving at this velocity. You produce that schedule. + +Reach for streaming when the trajectory is produced as the motion runs: a +teleoperation feed, a visual-servoing loop, a force-feedback correction on a +surface-finishing path, or a trajectory long enough that you want to keep only +part of it in memory. + +{{< alert title="Joint-space moves bypass the motion planner" color="caution" >}} +You are responsible for obstacle avoidance, constraint satisfaction, and path +smoothing. A trajectory point that puts the arm through the table or your +workspace fixture executes as sent. Everything you stream must already be safe. +{{< /alert >}} + +`MoveThroughJointPositionsStreamed` is safety-heartbeat monitored: if the +session that last called it stops sending heartbeats, `viam-server` stops the +arm. A client that dies mid-trajectory leaves the arm stopped. + +## Prerequisites + +- A configured arm component and an SDK client. +- An arm module that implements streaming. Streaming is currently supported for the [`viam:ufactory`](https://app.viam.com/module/viam/ufactory) and [`viam:universal-robots`](https://app.viam.com/module/viam/universal-robots) modules. +- A source of trajectory points that stays within the arm's joint limits. + +## What a trajectory point contains + +A trajectory point carries the following: + +- **Time**, measured from the start of the motion. The first point must be + zero, and every point after it must be strictly later than the one before. +- **Positions**, one value per joint, matching the arm's degrees of freedom. +- **Constraints**, optional and set per point. A point either carries no constraints at all or carries a velocity for every joint, with accelerations an optional addition on top. The arm starts from rest, so the first point is the arm standing still: either leave constraints off that point, or give every joint a velocity of zero. Only velocities have to be zero there; an acceleration on the first point is allowed. + +Each SDK uses different units for positions and constraints. For reference, see [Units](/motion-planning/move-an-arm/move-by-joint-positions/#units-python-uses-degrees-go-uses-radians). + +## How batching works + +A batch is a request of one or more trajectory points. Point times are offsets +from the start of the motion, so the same trajectory sent whole and sent in +three batches produces the same motion. Batching controls delivery; the point +times control the arm. + +Batch size controls how far ahead of the arm you commit. A point you +have sent is final: points cannot be replaced or revoked. Large batches cost +fewer round trips and leave more of the trajectory queued if your producer +falls behind, at the price of a longer committed stretch. Small batches keep +the last committed point close to where the arm is now, so a fresh sensor +reading can still change the next move. + +## Stream a trajectory + +Each SDK exposes the same stream through a different control flow. Python takes +an async iterator and gives you one back. Go hands you two channels that you +own. + +{{< alert title="SDK availability" color="caution" >}} +`MoveThroughJointPositionsStreamed` is available in the **Python, Go, and C++ +SDKs**. The TypeScript SDK does not expose it yet. +{{< /alert >}} + +{{< tabs >}} +{{% tab name="Python" %}} + +`move_through_joint_positions_streamed` takes an async iterator of batches and +returns an async iterator of `Arm.TrajectoryUpdate` values. Iterate the result +to read updates as the arm works through the trajectory. Each list you yield +becomes one `TrajectoryBatch` request. + +```python +from datetime import timedelta + +from viam.components.arm import Arm + +my_arm = Arm.from_robot(machine, "my-arm") + +# Times are offsets from the start of the motion; positions are degrees. +first_batch = [ + Arm.TrajectoryPoint( + time=timedelta(0), + positions=[0, -45, 90, 0, 45, 0], + # Velocities on the t=0 point must be zero. + constraints=Arm.KinematicConstraints(velocities=[0, 0, 0, 0, 0, 0]), + ), + Arm.TrajectoryPoint( + time=timedelta(milliseconds=500), + positions=[0, -22.5, 90, 0, 22.5, 0], + ), + Arm.TrajectoryPoint( + time=timedelta(seconds=1), + positions=[0, 0, 90, 0, 0, 0], + ), +] + + +async def batches(): + # Yield a list per batch. Returning ends the trajectory. + yield first_batch + + +async for update in my_arm.move_through_joint_positions_streamed(batches()): + # Updates arrive as the arm executes. Stopping this loop early closes + # the stream. + pass +``` + +{{% /tab %}} +{{% tab name="Go" %}} + +You create both channels. Write batches to `batches` and close it to end the +motion. Drain `responses` for the life of the call, because the client blocks +while it waits to hand one over, and close it after the call returns. + +```go +import ( + "math" + "time" + + "go.viam.com/rdk/components/arm" + "go.viam.com/rdk/referenceframe" +) + +batches := make(chan []arm.TrajectoryPoint) +responses := make(chan arm.Response) + +// The arm is free to acknowledge nothing at all, so this goroutine drains the +// channel rather than tracking progress. +go func() { + for range responses { + } +}() + +go func() { + defer close(batches) + + // Give up if the call returns early, so this goroutine never blocks on a + // channel nobody is reading. + send := func(b []arm.TrajectoryPoint) bool { + select { + case batches <- b: + return true + case <-ctx.Done(): + return false + } + } + + // Ten waypoints, 100ms apart, sent five at a time. + batch := make([]arm.TrajectoryPoint, 0, 5) + for i := 0; i < 10; i++ { + batch = append(batch, arm.TrajectoryPoint{ + Time: time.Duration(i*100) * time.Millisecond, + // Revolute joint values are radians, matching referenceframe.Input. + Positions: []referenceframe.Input{ + 0, -math.Pi/4 + float64(i)*math.Pi/40, math.Pi / 2, 0, math.Pi / 4, 0, + }, + }) + if len(batch) == 5 { + if !send(batch) { + return + } + batch = make([]arm.TrajectoryPoint, 0, 5) + } + } + if len(batch) > 0 { + send(batch) + } +}() + +// Blocks until the arm finishes the trajectory, the stream fails, or another +// operation cancels it. +err := myArm.MoveThroughJointPositionsStreamed(ctx, batches, responses, nil) +close(responses) +if err != nil { + logger.Fatal(err) +} +``` + +{{% /tab %}} +{{< /tabs >}} + +## Troubleshooting + +{{< expand "Streamed trajectory behaves oddly across a batch boundary" >}} + +Times are offsets from the start of the whole motion, not from the start of the +batch they arrive in. The first point of the stream must be at time zero and +every later point must be strictly greater than the one before it, across batch +boundaries as well as within a batch. Restarting the clock at each batch sends +the arm a trajectory that goes backwards in time. + +`viam-server` leaves enforcement to the arm module, so what a violation looks +like depends on the module: an error, a refused batch, or unintended motion. + +{{< /expand >}} + +{{< expand "Streamed trajectory fails with a joint range error" >}} + +The error names the joint index and the range it violated, not which trajectory +point carried it: `joint 1 needs to be within range [-360, 360] and cannot be +moved to 400`. Check the whole trajectory against the joint limits before you +start streaming to find the point at fault. + +The Go client raises this one as it encodes each point, the same check the +unary path makes, advancing through the trajectory point by point. Earlier +batches are already in flight by the time a bad point appears, so a rejected +point tears the whole stream down instead of returning an error for that +point alone. If the arm's kinematics are unregistered, the client logs a warning +and skips the check. + +{{< /expand >}} + +{{< expand "Wrong number of values error" >}} + +The `positions` array must match the arm's degrees of freedom. A 6-DOF +arm expects six values, a 7-DOF arm expects seven. Check the arm +module's documentation or the kinematics file. + +{{< /expand >}} + +## What's next + +- [Move by joint positions](/motion-planning/move-an-arm/move-by-joint-positions/): + the unary methods, for trajectories you have in hand before the arm moves. +- [Move an arm to a pose](/motion-planning/move-an-arm/move-to-pose/): + Cartesian motion with obstacle avoidance through `motion.Move`. +- [Arm kinematics](/motion-planning/reference/kinematics/): the + kinematic file that declares joint limits. diff --git a/static/include/components/apis/generated/arm.md b/static/include/components/apis/generated/arm.md index ea2b83a4af..22a1c16cfb 100644 --- a/static/include/components/apis/generated/arm.md +++ b/static/include/components/apis/generated/arm.md @@ -382,6 +382,37 @@ Stream batches of timed trajectory points to the arm and execute them in order a Unlike `MoveThroughJointPositions`, the full trajectory does not have to be known before the motion starts: the caller keeps appending points while the arm executes the ones it already has. {{< tabs >}} +{{% tab name="Python" %}} + +**Parameters:** + +- `batches` ([AsyncIterator[List[viam.components.arm.Arm.TrajectoryPoint]]](https://python.viam.dev/autoapi/viam/components/arm/index.html#viam.components.arm.Arm.TrajectoryPoint)) (required): an asynchronous iterator of lists of TrajectoryPoint. Each list becomes one wire TrajectoryBatch. +- `extra` (Mapping[[str](https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str), Any]) (optional): Extra options to pass to the underlying RPC call. +- `timeout` ([float](https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex)) (optional): An option to set how long to wait (in seconds) before calling a time-out and closing the underlying RPC call. + +**Returns:** + +- ([AsyncIterator[viam.components.arm.Arm.TrajectoryUpdate]](https://python.viam.dev/autoapi/viam/components/arm/index.html#viam.components.arm.Arm.TrajectoryUpdate)): : the arm’s updates, yielded as they arrive. + +**Example:** + +```python {class="line-numbers linkable-line-numbers"} +my_arm = Arm.from_robot(robot=machine, name="my_arm") + +async def batches(): + yield [ + Arm.TrajectoryPoint(time=timedelta(seconds=0.0), positions=[0.0, 0.0, 0.0, 0.0, 0.0]), + Arm.TrajectoryPoint(time=timedelta(seconds=1.0), positions=[10.0, 0.0, 0.0, 0.0, 0.0]), + ] + +async for update in my_arm.move_through_joint_positions_streamed(batches()): + # Observe the arm's updates; a fault raises out of this iteration. + pass +``` + +For more information, see the [Python SDK Docs](https://python.viam.dev/autoapi/viam/components/arm/client/index.html#viam.components.arm.client.ArmClient.move_through_joint_positions_streamed). + +{{% /tab %}} {{% tab name="Go" %}} **Parameters:** From de6589213605bfc70c42b978de8e598c84cdb894 Mon Sep 17 00:00:00 2001 From: Eliza Farley Date: Thu, 17 Sep 2026 11:09:58 -0400 Subject: [PATCH 3/3] DOCS-4801: Harden Go channel parameter parsing in update_sdk_methods.py Unescape HTML entities before matching channel direction tokens, clear per-parameter state to prevent neighbor-leaking, and add a fallback for unhandled parameter shapes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/update_sdk_methods.py | 35 ++++++++++++++++++------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/.github/workflows/update_sdk_methods.py b/.github/workflows/update_sdk_methods.py index 43eefe1587..c341a0a41b 100755 --- a/.github/workflows/update_sdk_methods.py +++ b/.github/workflows/update_sdk_methods.py @@ -538,22 +538,29 @@ def parse_method_usage(usage_string): param_type_link = "https://pkg.go.dev/builtin#error" else: param_raw = regex.sub(r'<.*?>', '', param).removesuffix(')').split() - ## Handle channel data types (only used for Board > StreamTicks): - if len(param_raw) == 3 and param_raw[0] == 'ch': - type_name = 'ch chan' - param_type = 'Tick' - type_link = '#Tick' - ## Handle directional channel params, such as Arm > MoveThroughJointPositionsStreamed's - ## 'batches <-chan []TrajectoryPoint'. The direction arrow arrives HTML-escaped and is - ## left that way so the rendered link text shows the arrow instead of swallowing it: - elif len(param_raw) == 3 and 'chan' in param_raw[1]: + + ## pkg.go.dev HTML-escapes the arrows in channel types, so put them back + ## before we match on the tokens: + param_raw = [token.replace('<', '<').replace('>', '>') for token in param_raw] + + ## Clear the per-parameter state. Python scopes these to the whole function, + ## so a parameter shape matching none of the cases below would otherwise + ## inherit the previous parameter's values and document itself as a copy of + ## its neighbor: + type_name = None + param_type = None + type_link = None + + ## Handle channel parameters, whose type spans two tokens: a direction + ## marker and the element type. All three directions occur in the SDK, + ## and the element type can itself be a slice: + if len(param_raw) == 3 and param_raw[1] in ('chan', '<-chan', 'chan<-'): type_name = param_raw[0] param_type = param_raw[1] + ' ' + param_raw[2] try: type_link = regex.findall(r'href="([^"]+)">', param)[-1] except: print("DEBUG: No type link found: {}, {}".format(usage_string, param)) - type_link = None ## Handle named parameters: elif len(param_raw) == 2: type_name = param_raw[0] @@ -600,6 +607,14 @@ def parse_method_usage(usage_string): except: print("DEBUG: No type link found: {}, {}, {}".format(usage_string, param, param_raw)) + ## Nothing above claimed this parameter. Fall back to the stripped source + ## text so the shape that got missed is visible in the output and in the + ## log, rather than quietly taking on its neighbor's identity: + if type_name is None and param_type is None: + print("DEBUG: Unhandled parameter shape: {}, {}".format(param, param_raw)) + type_name = '' + param_type = ' '.join(param_raw) + if type_link: param_type_link = type_link else: