BB.Motion (bb v0.30.1)

Copy Markdown View Source

High-level motion primitives that bridge IK solving and actuator commands.

This module provides functions for moving robot end-effectors to target positions using pluggable IK solvers. It handles the full workflow: solving IK and sending the resulting positions to actuators.

Usage

Single-target functions:

  • move_to/4 - Solve IK for one target and send the actuator commands
  • solve_only/4 - Solve IK without sending commands (for planning/validation)

Multi-target functions (for coordinated motion like gait):

Utility:

Context Sources

Functions accept either:

  • A robot module (uses Runtime to get robot and state)
  • A BB.Command.Context struct (uses context fields directly)

The second form is useful when implementing custom commands that need to perform IK-based motion.

Examples

# Single target
case BB.Motion.move_to(MyRobot, :gripper, {0.3, 0.2, 0.1},
       source_link: :base_link, solver: BB.IK.FABRIK) do
  {:ok, meta} -> IO.puts("Reached target in #{meta.iterations} iterations")
  {:error, %{class: :kinematics} = error} -> IO.puts("Failed: #{Exception.message(error)}")
end

# Multiple targets (for gait, coordinated motion)
targets = %{left_foot: {0.1, 0.0, 0.0}, right_foot: {-0.1, 0.0, 0.0}}
case BB.Motion.move_to_multi(MyRobot, targets,
       source_link: :body, solver: BB.IK.FABRIK) do
  {:ok, results} -> IO.puts("All targets reached")
  {:error, error} -> IO.puts("Failed: #{Exception.message(error)}")
end

# In a custom command handler
def handle_command(%{target: target}, context) do
  case BB.Motion.move_to(context, :gripper, target,
         source_link: :base_link, solver: BB.IK.FABRIK) do
    {:ok, meta} -> {:ok, %{residual: meta.residual}}
    {:error, error} -> {:error, error}
  end
end

# Just solve without moving (for validation)
case BB.Motion.solve_only(MyRobot, :gripper, {0.3, 0.2, 0.1},
       source_link: :base_link, solver: BB.IK.FABRIK) do
  {:ok, positions, meta} -> IO.inspect(positions, label: "Would set")
  {:error, %BB.Error.Kinematics.Unreachable{}} -> IO.puts("Cannot reach target")
end

# Send pre-computed positions
positions = %{shoulder: 0.5, elbow: 1.2}
:ok = BB.Motion.send_positions(MyRobot, positions, delivery: :direct)

Summary

Types

Why a motion stopped: either the solver couldn't reach the target, or an actuator refused the command it was sent.

Functions

Move an end-effector to a target position.

Move multiple end-effectors to target positions simultaneously.

Send pre-computed joint positions to actuators.

Solve IK without moving the robot.

Solve IK for multiple targets without moving the robot.

Types

delivery()

@type delivery() :: :pubsub | :direct

kinematics_error()

@type kinematics_error() :: BB.IK.Solver.kinematics_error()

meta()

@type meta() :: BB.IK.Solver.meta()

motion_error()

@type motion_error() :: kinematics_error() | BB.Error.t()

Why a motion stopped: either the solver couldn't reach the target, or an actuator refused the command it was sent.

motion_result()

@type motion_result() :: {:ok, meta()} | {:error, motion_error()}

multi_motion_result()

@type multi_motion_result() :: {:ok, multi_results()} | {:error, motion_error()}

multi_results()

@type multi_results() :: %{
  required(atom()) => {:ok, positions(), meta()} | {:error, kinematics_error()}
}

multi_solve_result()

@type multi_solve_result() ::
  {:ok, multi_results()} | {:error, BB.Error.Kinematics.MultiFailed.t()}

positions()

@type positions() :: BB.IK.Solver.positions()

robot_or_context()

@type robot_or_context() :: module() | BB.Command.Context.t()

solve_result()

@type solve_result() :: {:ok, positions(), meta()} | {:error, kinematics_error()}

target()

@type target() :: BB.IK.Solver.target()

targets()

@type targets() :: %{required(atom()) => target()}

Functions

move_to(robot_or_context, target_link, target, opts)

@spec move_to(robot_or_context(), atom(), target(), keyword()) :: motion_result()

Move an end-effector to a target position.

Solves inverse kinematics for the given target and sends position commands to all actuators controlling the affected joints.

Where the joints actually end up is reported by their sensors, so this does not write the solved positions into BB.Robot.State - a commanded position is not a measured one. A joint whose actuator has no position feedback wants a BB.Sensor.OpenLoopPositionEstimator.

Options

Required:

  • :solver - Module implementing BB.IK.Solver behaviour
  • :source_link - The link the chain starts at. No default: the root is right for a fixed-base arm and silently wrong for a robot whose base floats, so pass BB.Robot.root_link(robot) when you do mean the whole tree

Optional:

  • :delivery - How to send actuator commands. :pubsub (default) publishes each command and waits for the actuator to accept it, reporting the first refusal; :direct casts to each actuator and waits for nothing, so a refusal is never reported
  • :velocity - Velocity hint (passed to actuators)
  • :duration - Duration hint in milliseconds (passed to actuators)
  • :command_id - Correlation ID for feedback tracking (passed to actuators)
  • :timeout - How long to wait for each actuator to accept its command, in milliseconds (default 5000). Unused under :direct, which waits for nothing. A timeout exits the caller, as GenServer.call/3 does — a loop that would rather skip a late step than die wants :direct
  • :max_iterations - Maximum solver iterations (passed to solver)
  • :tolerance - Convergence tolerance in metres (passed to solver)
  • :respect_limits - Whether to clamp to joint limits (passed to solver)

Returns

  • {:ok, meta} - Successfully moved; meta contains solver info (iterations, residual, etc.)
  • {:error, error} - Failed; either a struct from BB.Error.Kinematics if the target couldn't be solved, or the actuator's own error if one refused the command it was sent

Examples

BB.Motion.move_to(MyRobot, :gripper, {0.3, 0.2, 0.1},
  source_link: :base_link,
  solver: BB.IK.FABRIK
)

BB.Motion.move_to(context, :gripper, target,
  source_link: :base_link,
  solver: BB.IK.FABRIK,
  delivery: :direct,
  max_iterations: 100
)

move_to_multi(robot_or_context, targets, opts)

@spec move_to_multi(robot_or_context(), targets(), keyword()) :: multi_motion_result()

Move multiple end-effectors to target positions simultaneously.

Useful for coordinated motion like walking gaits where multiple limbs must move together. Each target is solved independently and all actuator commands are sent together.

If any target fails to solve, the operation stops and returns an error with information about which target failed. Targets solved before the failure are included in the results.

Options

Required:

  • :solver - Module implementing BB.IK.Solver behaviour
  • :source_link - The link the chain starts at. No default: the root is right for a fixed-base arm and silently wrong for a robot whose base floats, so pass BB.Robot.root_link(robot) when you do mean the whole tree

Optional:

  • :delivery - How to send actuator commands. :pubsub (default) publishes each command and waits for the actuator to accept it, reporting the first refusal; :direct casts to each actuator and waits for nothing, so a refusal is never reported
  • :velocity - Velocity hint (passed to actuators)
  • :duration - Duration hint in milliseconds (passed to actuators)
  • :command_id - Correlation ID for feedback tracking (passed to actuators)
  • :timeout - How long to wait for each actuator to accept its command, in milliseconds (default 5000). Unused under :direct, which waits for nothing. A timeout exits the caller, as GenServer.call/3 does — a loop that would rather skip a late step than die wants :direct
  • :max_iterations - Maximum solver iterations (passed to solver)
  • :tolerance - Convergence tolerance in metres (passed to solver)
  • :respect_limits - Whether to clamp to joint limits (passed to solver)

Returns

  • {:ok, results} - All targets solved; results is a map of link → {:ok, positions, meta}
  • {:error, %BB.Error.Kinematics.MultiFailed{}} - A target failed to solve. The error names the link that failed, carries the underlying kinematics error, and keeps the results of the targets solved before it
  • {:error, error} - Every target solved, but an actuator refused the command it was sent. That failure isn't kinematic, so it arrives as the actuator's own error rather than wrapped in MultiFailed

Examples

targets = %{
  left_foot: {0.1, 0.0, 0.0},
  right_foot: {-0.1, 0.0, 0.0}
}

case BB.Motion.move_to_multi(MyRobot, targets,
       source_link: :body, solver: BB.IK.FABRIK) do
  {:ok, results} ->
    IO.puts("All targets reached")

  {:error, %MultiFailed{failed_link: link} = error} ->
    IO.puts("Failed to reach #{link}: #{Exception.message(error)}")

  {:error, error} ->
    IO.puts("An actuator refused: #{Exception.message(error)}")
end

send_positions(robot_or_context, positions, opts \\ [])

@spec send_positions(robot_or_context(), positions(), keyword()) ::
  :ok | {:error, motion_error()}

Send pre-computed joint positions to actuators.

Bypasses IK solving entirely - useful when you've already computed positions through other means (e.g., trajectory planning, manual input).

Sends commands to all actuators controlling the specified joints. As with move_to/4, the robot's own state is left to its sensors.

Options

  • :delivery - How to send actuator commands. :pubsub (default) publishes each command and waits for the actuator to accept it, reporting the first refusal; :direct casts to each actuator and waits for nothing, so a refusal is never reported
  • :velocity - Velocity hint for actuators (rad/s or m/s)
  • :duration - Duration hint for actuators (milliseconds)
  • :command_id - Correlation ID for feedback tracking
  • :timeout - How long to wait for each actuator to accept its command, in milliseconds (default 5000). Unused under :direct, which waits for nothing. A timeout exits the caller, as GenServer.call/3 does — a loop that would rather skip a late step than die wants :direct

Returns

  • :ok - Every actuator accepted its command
  • {:error, error} - One refused; the rest were still sent

Examples

positions = %{shoulder: 0.5, elbow: 1.2, wrist: 0.3}
:ok = BB.Motion.send_positions(MyRobot, positions)

# With direct delivery for lower latency
:ok = BB.Motion.send_positions(MyRobot, positions, delivery: :direct)

solve_only(robot_or_context, target_link, target, opts)

@spec solve_only(robot_or_context(), atom(), target(), keyword()) :: solve_result()

Solve IK without moving the robot.

Useful for:

  • Validating that a target is reachable before committing
  • Planning multi-step motions
  • Visualising solutions before execution

Options

Same as move_to/4 except :delivery is not used.

Returns

  • {:ok, positions, meta} - Successfully solved; positions is a joint name → value map
  • {:error, error} - Failed to solve; error is a struct from BB.Error.Kinematics

Examples

# Check if target is reachable
case BB.Motion.solve_only(MyRobot, :gripper, target,
       source_link: :base_link, solver: BB.IK.FABRIK) do
  {:ok, _positions, %{reached: true}} -> :reachable
  {:error, _} -> :unreachable
end

solve_only_multi(robot_or_context, targets, opts)

@spec solve_only_multi(robot_or_context(), targets(), keyword()) ::
  multi_solve_result()

Solve IK for multiple targets without moving the robot.

Useful for validating that a set of coordinated targets are all reachable before committing to motion.

Options

Same as move_to_multi/3 except :delivery is not used.

Returns

  • {:ok, results} - All targets solved; results is a map of link → {:ok, positions, meta}
  • {:error, %BB.Error.Kinematics.MultiFailed{}} - A target failed. The error names the link that failed, carries the underlying kinematics error, and keeps the results of the targets solved before it

Examples

targets = %{left_foot: {0.1, 0.0, 0.0}, right_foot: {-0.1, 0.0, 0.0}}

case BB.Motion.solve_only_multi(MyRobot, targets,
       source_link: :body, solver: BB.IK.FABRIK) do
  {:ok, results} ->
    Enum.each(results, fn {link, {:ok, _positions, meta}} ->
      IO.puts("#{link}: residual=#{meta.residual}")
    end)

  {:error, %MultiFailed{failed_link: link} = error} ->
    IO.puts("#{link} is unreachable: #{Exception.message(error)}")
end