Pose Manager

Applications accumulate named poses: a home pose, a pick position, a safe waypoint. The PoseManager stores such poses under names, so that they can be looked up, iterated, and exchanged through YAML files. It stores PoseTarget values, the same type the motion methods accept, so a stored pose is directly usable as a motion target.

Creating a Pose Manager and Adding Poses

Create a manager and add poses under unique names. Both JointPose and CartesianPose values are accepted. A CartesianTarget is accepted only with the defaults of a plain pose, since a pose file stores only the pose: another coordinate system, arriving frame or a configuration vector is rejected with UnstorableTarget, and a NaN or infinite value with InvalidPose. Adding a name that already exists is an error, not a silent overwrite:

let mut manager = PoseManager::new();
manager.add("home", poses::home())?;
manager.add(
    "pickup",
    CartesianPose::new(
        Meters(0.4),
        Meters(0.1),
        Meters(0.3),
        Radians(PI),
        Radians(0.0),
        Radians(0.0),
    ),
)?;

// Read the current robot joint pose and store it.
let current = robot.joint_pose()?;
manager.add("current", current)?;
info!("Added 3 poses to the manager.");

To replace a stored pose, for example when a point is taught again, use insert(). It returns the pose it replaced, or None for a new name:

// Teach 'current' again. Unlike `add`, `insert` overwrites an existing
// name, like `BTreeMap::insert`. It returns the old pose, or `None` for a
// new name.
let previous = manager.insert("current", robot.joint_pose()?)?;
info!("Re-taught 'current', replacing {previous:?}");

Retrieving Poses

A pose is looked up by name with get(). A miss is None. Because a PoseTarget is Copy, the stored pose can be handed straight to a motion:

let home = manager
    .get("home")
    .ok_or(ExampleError::PoseMissing("home"))?;
info!("Retrieved 'home': {home:?}");

// Use a stored pose as a motion target.
robot.move_ptp(*home).start()?.join()?;

All poses are iterated as pairs of name and pose:

info!("All poses:");
for (name, pose) in &manager {
    info!("  {name}: {pose:?}");
}

Removing Poses

remove() deletes a pose and returns it. Like get(), it returns None for an unknown name:

let removed = manager.remove("current");
info!(
    "Removed 'current' ({removed:?}). Remaining: {} poses.",
    manager.len()
);

YAML Export and Import

All poses are written to a YAML file with export_yaml():

// A private directory keeps concurrent runs apart.
let temp_dir = tempdir()?;
let file_path = temp_dir.path().join("voraus_poses.yaml");
manager.export_yaml(&file_path)?;
info!("Exported poses to {}", file_path.display());

The file lists each pose with its name, its type, and its components:

poses:
  - name: home
    type: JointPose
    pose:
      - 0.0
      - -1.57
      - 1.57
      - -1.57
      - -1.57
      - 0.0

import_yaml() reads such a file into a manager. Its second argument says what happens to a name that already exists: OnConflict::Fail imports nothing and returns DuplicateName, like add(); OnConflict::KeepExisting keeps the stored pose; OnConflict::Overwrite lets the file win. A name that appears twice in the file, a NaN or infinite value, an unknown type, or an entry with the wrong number of values fails the whole import. Files with VorausPose entries import as well; these entries become a CartesianPose:

let mut imported = PoseManager::new();
imported.import_yaml(&file_path, OnConflict::Fail)?;
info!("Imported {} poses from YAML.", imported.len());

// Importing the same file again fails on the duplicate names with
// `OnConflict::Fail`; `Overwrite` lets the file win, `KeepExisting` the
// manager.
imported.import_yaml(&file_path, OnConflict::Overwrite)?;

to_yaml_string() and import_yaml_str() do the same with a string instead of a file, for poses embedded with include_str!, kept in a database or received over the network.

Full Example

Error Type

The methods of PoseManager are documented in the API reference, cargo doc --open -p voraus-robot-arm.