Phase 3: Static positions and obstacles
In this phase you will teach an arm to move through a set of named poses that together form a pick-and-place cycle, and you let Viam’s motion service create collision-free paths between them. First you will save each pose by hand, jogging the arm into position and recording where it is. Then you create obstacles so the Motion Service knows what to avoid when creating the collision free motion.
Why static positions first
When you add perception and motion planning at the same time, a failure could live in detection, the frame transform, the pose math, the motion planner, or gripper timing, and there is no straightforward way to tell which. Saving fixed poses lets you run the full hardware loop first. In the following phase, you drive this same proven sequence from a Python script. Once the arm reliably travels through every stage of the sequence, perception becomes the only new variable when you reach it.
Pose-to-pose motion without perception is a real production workcell pattern: any time a part always lands in the same spot, a fixed sequence of saved poses is simpler and more reliable than running detection on every cycle.
Each move in that sequence also validates one part of your setup, which the next section lays out pose by pose.
The key poses
You save five named poses. Run in order, they form one pick-and-place cycle:
home observe and rest, above the workspace
│
▼
approach standoff directly above the block
│ gripper.open()
▼
grasp down at the block
│ gripper.grab()
▼
travel lift clear of obstacles
│
▼
place above the bin
│ gripper.open()
▼
home back to the start
Each pose has a specific role, and reaching it cleanly validates one part of your setup:
| Pose | Purpose | What reaching it validates |
|---|---|---|
| home-pose | Observation position above the workspace; the wrist camera has a clear view of the blocks. | Observation position is safe and repeatable |
| approach-pose | Standoff directly above the pick zone, roughly 80 to 100 mm above the highest block | Arm can get above the workspace without collision |
| grasp-pose | At the block, gripper open and ready to close; fingertips are level with the block top | Descent distance is correct and the gripper’s finger timing works |
| travel-pose | Safe carrying height that clears obstacles while holding a block | Safe carrying height clears obstacles |
| place-pose | Above the bin where blocks are dropped | Bin position is correct |
The approach pose and the grasp pose share the same x and y coordinates. The only motion between them is straight down the z axis, so if the arm drifts sideways during the descent you have a frame or calibration issue to investigate.
If your bin is a rectangle or box shape and it’s visible in home pose, the vision service may try to pick it up. To be safe, place the bin outside the camera’s view in home pose.
Save each pose with the arm position saver
You configure pose saving by hand, the same way you configured the arm, gripper, and camera in Phases 1 and 2.
On the Configure tab, click the + icon and select Blocks. Search for arm-position-saver, select the vmodutils/arm-position-saver result, and name it home-pose. This is the first model you use from the erh:vmodutils module, so viam-server downloads that module now; the same module also provides the vmodutils/obstacle model you configure later in this phase, so it downloads only once.
The arm-position-saver is a switch: a resource whose numbered positions each trigger an action instead of reporting a value. You use two of its positions, one to save the arm’s current joint positions (“update config”) and one to replay them (“go to”). Throughout this phase, “the switch” refers to this pose-saver resource.
Set one attribute:
{
"arm": "arm-1"
}
This attribute is also a dependency, the same way gripper-1 depends on arm-1: the switch cannot save or recall a pose until the arm it points at is running.
The arm will move
The steps in this phase move the physical arm, both when you jog it into position and when you set a switch to “go to” (position 2) to replay a saved pose. Keep the workspace clear and the e-stop within reach. Verify each pose individually before you run the full sequence at the end of the phase.
With the home-pose switch added, save and verify it. Home is your observation pose, so jog the arm to a spot above the workspace where the wrist camera has a clear, unobstructed view of the blocks; Phase 5 detects from exactly this pose.
You have three ways to jog the arm on its Control card, and you can mix them:
Joint control (
MoveToJointPositions) sets each joint angle directly. Move one joint slider a small amount, press Execute, and that joint rotates. It is the most predictable way to make coarse changes and to lift the arm clear before repositioning. You can also use quick move mode (indicated by the lightning bolt icon) to move a joint five degrees at a time.End-effector control (
MoveToPosition) moves the tip of the arm to a Cartesian target instead of setting joints. Press Current position to load the arm’s current pose into the fields, then change a coordinate and press Execute. The values are millimeters in the world frame at the arm base: raising or lowering z moves the gripper straight up or down, while x and y slide it horizontally across the workspace. Change one value by a small amount and watch the arm, or the 3D scene, to learn which way each axis points for your setup. Use this to nudge the gripper in a specific direction without solving for joint angles.Manual mode lets you physically manipulate the arm to a desired position. To enter manual mode, go to the arm’s Control card, then find the Do command section.
DoCommandis a generic method that Viam components and services expose for functionality outside the standard API, letting you send arbitrary commands from the Control tab or your code without needing a dedicated method for every action. In the Input panel, enter:{ "enter_manual_mode": true }Press Execute. The Output should read,
"status": "entered manual mode"and you should be able to easily move the arm by hand.
Run these four steps to save and verify the pose:
- On the arm test card, jog the arm into position using joint control, end-effector control, manual mode, or a mix of the three.
- Under MoveToPosition, click Current position and note the x, y, and z values to confirm the arm is where you expect it.
- On the switch test card, click update config to save the current joint positions.
- To validate, move the arm away to a different position. From the switch test card, click go to and confirm the arm returns to the saved pose.
Setting the switch to update config writes the current joint positions straight into the switch’s own configuration. Unlike the components you added in Phases 1 and 2, there is no separate Save step here: the pose is persisted as soon as you trigger update config, and you can see the saved joint values appear in the switch’s config JSON:
{
"arm": "arm-1",
"joints": [
0.0000025790882318688095, -0.7929777503013611, -0.8206289410591125,
-6.174358873067831e-7, 1.611369013786316, 2.2541650324114926e-8
],
"motion": "",
"vision_services": [],
"constraints": {},
"extra": {}
}
The joints array holds the six joint angles, in radians, captured the moment you triggered update config; arm is the dependency you set earlier, and the remaining fields stay at their defaults for this workshop. Triggering update config again overwrites joints with wherever the arm is now, which is why you jog to the pose you want before saving.

Now that home-pose is saved, open its resource card on the Configure tab and use the Duplicate feature to create a copy. Rename the copy to approach-pose, and its arm attribute carries over automatically since it is already set to "arm-1". Duplicate three more times for grasp-pose, travel-pose, and place-pose. This is faster than adding five switches from scratch and less error-prone, since you only type the arm attribute once.

Run the same four save-and-verify steps for each of the four new poses: jog the arm into position, confirm it with Current position under MoveToPosition, set the switch to “update config” to save, and set it to “go to” to confirm the arm returns. Where you jog to for each one is not arbitrary: use the Purpose column in the table above as your target for each of the five key poses, not just any reachable spot.
Switch positions
On an arm-position-saver switch, position 1 (update config) saves the current joint positions and position 2 (go to) moves the arm to the saved pose. Position 0 is the idle resting state the switch returns to after a save or a move; it does not clear the saved pose. Always save with position 1 before you attempt position 2. Setting position 2 on an unsaved switch does nothing.
Checkpoint
home-pose still gives the wrist camera a clear view of the blocks. If a switch does nothing when you set it to position 2, you have not saved it yet; set position 1 first, then try position 2 again.Teach the planner about obstacles
The Viam motion planner is collision-aware, but it can only avoid geometry it knows about. Without any obstacle configuration, the planner avoids self-collisions only. Once you add obstacle geometry, the planner treats the table surface and the workspace boundary as hard obstacles it cannot plan through.
In this workshop you configure two types of obstacles: the table surface and two safety walls at the workspace boundary.
Obstacles as components
An obstacle can be configured as a vmodutils/obstacle component you add on the Configure tab, the same way you added the arm, gripper, and camera. This obstacle model uses the gripper API, so once configured, each obstacle has the same control UI as a gripper. This is purely as a resource container for geometry.
The obstacle geometry is then automatically included in the world state the motion service uses to plan a safe path for the arm to a target position in 3D space. This is one of two ways to get obstacle geometry into that world state: configuring it here, as a component, means it persists on the machine and applies to every move, which is what a fixed table and fixed walls call for. The other way, passing a WorldState directly on a single motion.move call in code, suits geometry that only matters for one move and should not persist, and is out of scope for this workshop; see Move an arm if you need that pattern later.
Setup goal
The 3D scene below is the goal for this section: the table surface and two safety walls rendered around the arm, so the motion planner treats them as hard boundaries it cannot plan through.

Add the table obstacle
Start with the table.
Click the + icon and select Blocks, then search for obstacle and select the vmodutils/obstacle result. Name it table. In the attributes editor, paste the geometries blob:
{
"geometries": [
{
"type": "box",
"x": 1200,
"y": 800,
"z": 30
}
]
}
This defines the table as a box with placeholder dimensions 1200 x 800 x 30 millimeters. Replace these with your own table’s length, width, and thickness; Position the obstacles below covers when and how to swap in your own measurements.
Next click Frame and set the frame that positions the table obstacle in the world.
parent is world, and translation is where the box center sits relative to the world origin, which in this setup is the arm base:
{
"parent": "world",
"translation": { "x": 0, "y": 0, "z": -15 },
"orientation": {
"type": "ov_degrees",
"value": { "x": 0, "y": 0, "z": 1, "th": 0 }
}
}
The one detail that trips people up is that translation.z is the box’s center, not its top or bottom surface. The world origin sits at table-top height (z = 0), so a 30 mm thick table needs translation.z of -15, half its thickness: the box extends from -30 up to 0, and its center is at -15. The safety walls you add next use the same rule in the other direction: a 600 mm tall wall gets translation.z of 300 so it rises from 0 to 600. An obstacle only appears in the 3D scene and the planner’s world state once it has this frame.
translation.x and translation.y above assume the arm sits at the table’s center, so treat 0, 0 as a placeholder too. Most workshop setups clamp the arm to one end of the table instead of rooting it at the center, so your real x and y will differ. Position the obstacles below covers how to dial those in.
Add the safety walls
Add two more vmodutils/obstacle components the same way, one per boundary you want to wall off. Each has its box dimensions in geometries and a world-parented frame that places it.
safety-wall-front attributes:
{
"geometries": [{ "type": "box", "x": 20, "y": 1200, "z": 600 }]
}
safety-wall-front frame:
{
"parent": "world",
"translation": {
"x": 600,
"y": 0,
"z": 300
},
"orientation": {
"type": "ov_degrees",
"value": { "x": 0, "y": 0, "z": 1, "th": 0 }
}
}
safety-wall-back attributes:
{
"geometries": [{ "type": "box", "x": 20, "y": 1200, "z": 600 }]
}
safety-wall-back frame:
{
"parent": "world",
"translation": {
"x": -600,
"y": 0,
"z": 300
},
"orientation": {
"type": "ov_degrees",
"value": { "x": 0, "y": 0, "z": 1, "th": 0 }
}
}
600 and -600 are placeholders sized for a 1200 mm long table with the arm at its center, so the front and back walls sit at the two ends. Both walls are 600 mm tall, so their frame translation.z is 300, half the height; that value depends only on the wall height you measure, not on where the arm sits, so it does not need adjustment below.
Position the obstacles
The dimensions and translations for the table and walls above are placeholders. Replace them with your own in two passes:
- Tape-measure the box sizes. Measure your table’s length, width, and thickness, and the length and height of your workspace boundary. These go straight into each obstacle’s
geometriesblock (x,y,z), replacing the placeholder numbers above. - Use the 3D scene tab to place the boxes. An arm is rarely mounted at the exact center of a table; most workshop setups clamp it to one end instead. Open the 3D scene tab, then in the upper right corner click the hammer icon to enter build mode. From build mode, select an obstacle from the World panel. You can then use the red arrows to drag the obstacle into a position that mirrors your workspace. This is a visual calibration against what you see in the 3D scene, not a formula to solve.
You can check your obstacle configuration against the companion repo’s obstacles-template.json, which has the full set with example measurements filled in for an arm mounted in the center of a table. The full machine configuration, including all pose switches and obstacles, is in machine-fragment.json. Treat both as references to check your work against, not as files to import over what you configured by hand.
Test the full static sequence
From the Control tab, trigger the pose switches in this order:
home-pose (2) -> approach-pose (2) -> Open gripper ->
grasp-pose (2) -> Grab -> travel-pose (2) ->
place-pose (2) -> Open gripper -> home-pose (2)
The Open and Grab buttons are the same gripper controls you used in Phase 2: Grab closes the fingers on a block and Open releases it.
As the arm moves, open the 3D scene tab to watch its path alongside the table surface and the safety walls.
The planner refuses to plan through configured geometry, so an obstacle conflict shows up as a planning failure in the logs, not as the arm passing through the obstacle. Open the Logs tab alongside the 3D scene to catch any such planning failure in real time.
Checkpoint
You now have a working static sequence. In Phase 4 you drive this same sequence from a Python script, replacing the manual switch triggers with code.
Was this page helpful?
Glad to hear it! If you have any other feedback please let us know:
We're sorry about that. To help us improve, please tell us what we can do better:
Thank you!