From d8e5c702735ad16d3b5000940fd71121d80b354c Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 17:27:35 -0400 Subject: [PATCH 01/44] feat(tutorials): add checkpoint shortcode for workshop callouts --- layouts/shortcodes/checkpoint.html | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 layouts/shortcodes/checkpoint.html diff --git a/layouts/shortcodes/checkpoint.html b/layouts/shortcodes/checkpoint.html new file mode 100644 index 0000000000..4e10dd3c27 --- /dev/null +++ b/layouts/shortcodes/checkpoint.html @@ -0,0 +1,20 @@ +{{/* checkpoint.html + A "verify before continuing" callout for sequential workshop tutorials. + CSS is injected once per page via a Scratch guard (mirrors notice.html). */}} +{{- $title := .Get "title" | default "Checkpoint" -}} +{{- $inner := .Inner | .Page.RenderString -}} +{{- if not ($.Page.Scratch.Get "checkpointcss") -}} + +{{- $.Page.Scratch.Set "checkpointcss" 1 -}} +{{- end -}} +
+

{{ $title }}

+
{{ $inner }}
+
From 940c16a2ce9b0064f42635b6659463275bf8e16d Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 17:27:58 -0400 Subject: [PATCH 02/44] feat(tutorials): add workshop-nav shortcode for phase progress and prev/next --- layouts/shortcodes/workshop-nav.html | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 layouts/shortcodes/workshop-nav.html diff --git a/layouts/shortcodes/workshop-nav.html b/layouts/shortcodes/workshop-nav.html new file mode 100644 index 0000000000..ba9e29cf5c --- /dev/null +++ b/layouts/shortcodes/workshop-nav.html @@ -0,0 +1,27 @@ +{{/* workshop-nav.html + Progress indicator + prev/next links for sequential workshop phases. + Reads page frontmatter: phase, phase_total, prev, next. */}} +{{- $phase := .Page.Params.phase -}} +{{- $total := .Page.Params.phase_total -}} +{{- $prev := .Page.Params.prev -}} +{{- $next := .Page.Params.next -}} +{{- if not ($.Page.Scratch.Get "workshopnavcss") -}} + +{{- $.Page.Scratch.Set "workshopnavcss" 1 -}} +{{- end -}} + From cd6fd4eeec98fd7c8f2996128fde052f2444150c Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 17:50:08 -0400 Subject: [PATCH 03/44] style(tutorials): align workshop shortcode dark-mode and icon naming with conventions --- layouts/shortcodes/checkpoint.html | 2 +- layouts/shortcodes/workshop-nav.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/layouts/shortcodes/checkpoint.html b/layouts/shortcodes/checkpoint.html index 4e10dd3c27..cd43128573 100644 --- a/layouts/shortcodes/checkpoint.html +++ b/layouts/shortcodes/checkpoint.html @@ -15,6 +15,6 @@ {{- $.Page.Scratch.Set "checkpointcss" 1 -}} {{- end -}}
-

{{ $title }}

+

{{ $title }}

{{ $inner }}
diff --git a/layouts/shortcodes/workshop-nav.html b/layouts/shortcodes/workshop-nav.html index ba9e29cf5c..b97bd3a291 100644 --- a/layouts/shortcodes/workshop-nav.html +++ b/layouts/shortcodes/workshop-nav.html @@ -13,6 +13,7 @@ .workshop-nav .workshop-nav-links a{display:inline-block;padding:8px 16px;border:1px solid #ccc;border-radius:4px;text-decoration:none;font-weight:600} .workshop-nav .workshop-nav-next{margin-left:auto} @media (prefers-color-scheme:dark){.workshop-nav{border-top-color:#444}.workshop-nav .workshop-progress{color:#aaa}.workshop-nav .workshop-nav-links a{border-color:#555}} +body.dark .workshop-nav{border-top-color:#444}body.dark .workshop-nav .workshop-progress{color:#aaa}body.dark .workshop-nav .workshop-nav-links a{border-color:#555} {{- $.Page.Scratch.Set "workshopnavcss" 1 -}} {{- end -}} From e38ed67b0056e84a9b85bb2861c26765dad48235 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 17:54:36 -0400 Subject: [PATCH 04/44] feat(tutorials): add pick-and-place workshop overview and phase template Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0138T5LK4cgYNgn3X54V7iZq --- docs/tutorials/pick-and-place/_index.md | 66 +++++++++++++++++++ .../pick-and-place/_phase-template.md | 29 ++++++++ 2 files changed, 95 insertions(+) create mode 100644 docs/tutorials/pick-and-place/_index.md create mode 100644 docs/tutorials/pick-and-place/_phase-template.md diff --git a/docs/tutorials/pick-and-place/_index.md b/docs/tutorials/pick-and-place/_index.md new file mode 100644 index 0000000000..79b031de84 --- /dev/null +++ b/docs/tutorials/pick-and-place/_index.md @@ -0,0 +1,66 @@ +--- +title: "Vision-Guided Pick-and-Place with the xArm6" +linkTitle: "Pick-and-Place Workshop" +type: "docs" +weight: 50 +description: "Build a robot that detects, picks, and sorts colored cubes using computer vision and motion planning, progressing from static positions to a local Python script to an optional on-robot module." +authors: [] +level: "Intermediate" +languages: ["python"] +viamresources: ["arm", "gripper", "camera", "vision", "motion", "frame_system", "switch"] +platformarea: ["mobility", "core"] +tags: ["tutorial", "workshop", "arm", "vision", "motion"] +workshop: "pick-and-place" +workshop_overview: true +time_estimate: "2 hours" +hardware: + - "uFactory xArm6" + - "Intel RealSense D435" + - "uFactory finger gripper" + - "System76 Meerkat" +companion_repo: "https://github.com/viam-devrel/pick-and-place" +no_list: true +draft: true +--- + +In this workshop you build a vision-guided robot that detects colored cubes, picks each one up, and drops it in the correct bin, sorted by color. The workshop is structured as five sequential phases, each ending with a working system state you can verify before moving on. Completing Phase 4 (the local Python script) is a full success; Phase 5 (packaging your script as an inline module) is optional. + +## What you'll build + +You will configure an xArm6 robotic arm fitted with a finger gripper and an Intel RealSense depth camera. A color-detection vision service identifies cube positions in camera space; the Viam motion service plans and executes collision-free arm movements to pick each cube and place it in the right bin. By the end of Phase 4 you have a Python script you can run from your laptop that drives the full pick-and-sort cycle autonomously. + + + +## Hardware + +- **uFactory xArm6**: the six-axis robotic arm that picks and places the cubes. +- **Intel RealSense D435**: the depth camera mounted to detect cube positions and colors. +- **uFactory finger gripper**: the end-effector that grasps the cubes. +- **System76 Meerkat**: the on-robot mini-PC that runs the Viam machine server. + +## Phases + +1. **Platform mental model** (~15 min): learn how Viam resources, the frame system, and the motion service fit together before touching any hardware. +2. **Configure resources and explore the app** (~20 min): add the arm, gripper, camera, and vision service in the Viam app; confirm each resource is live. +3. **Static positions and safety obstacles** (~20 min): define named arm poses and add a virtual obstacle to prevent the arm from hitting the table. +4. **Local Python script** (~30 min): write and run the pick-and-sort script from your laptop; this is the core goal of the workshop. +5. **Inline module** (~15 min, optional): package the script as an on-robot module so the cycle runs without a laptop connection. + +## Prerequisites + +**Hardware pre-provisioned for you:** if you are in a guided workshop where the hardware is already set up, skip directly to Phase 1. + +**Provisioning your own hardware:** complete the hardware setup guide first (forthcoming), then return here for Phase 1. The setup guide covers mounting the camera, connecting the arm controller, and installing viam-server on the Meerkat. + +Before Phase 4 you also need Python 3.10 or newer and the Viam Python SDK on the machine you will run the script from. Verify with: + +```sh +python3 --version # 3.10 or newer +python3 -c "import viam" # must succeed +``` + +## Companion code + +All supporting files for this workshop live in the [viam-devrel/pick-and-place](https://github.com/viam-devrel/pick-and-place) repository. It contains the machine config fragment you will import in Phase 2, the starter script for Phase 4, and the reference solution for Phase 5. + + diff --git a/docs/tutorials/pick-and-place/_phase-template.md b/docs/tutorials/pick-and-place/_phase-template.md new file mode 100644 index 0000000000..34475aa727 --- /dev/null +++ b/docs/tutorials/pick-and-place/_phase-template.md @@ -0,0 +1,29 @@ +--- +title: "Phase N: Title in under 70 characters" +linkTitle: "N. Short title" +type: "docs" +slug: "phase-slug" +weight: 0 # 10, 20, 30, ... in 10s for sidebar order +description: "One-sentence description of this phase." +workshop: "pick-and-place" +phase: 0 +phase_total: 5 +time_estimate: "NN minutes" +prev: "/tutorials/pick-and-place/previous-slug/" +next: "/tutorials/pick-and-place/next-slug/" +languages: ["python"] +draft: true +headless: true +--- + + + +## Section heading + + + +{{< checkpoint >}} +What the learner runs, and the expected result that means "continue". +{{< /checkpoint >}} + +{{< workshop-nav >}} From 1babb176c525ddd3127b3d3c60f931247fdb63f4 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 17:57:26 -0400 Subject: [PATCH 05/44] docs(tutorials): trim workshop overview description under SEO length --- docs/tutorials/pick-and-place/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/pick-and-place/_index.md b/docs/tutorials/pick-and-place/_index.md index 79b031de84..fa69eda2e5 100644 --- a/docs/tutorials/pick-and-place/_index.md +++ b/docs/tutorials/pick-and-place/_index.md @@ -3,7 +3,7 @@ title: "Vision-Guided Pick-and-Place with the xArm6" linkTitle: "Pick-and-Place Workshop" type: "docs" weight: 50 -description: "Build a robot that detects, picks, and sorts colored cubes using computer vision and motion planning, progressing from static positions to a local Python script to an optional on-robot module." +description: "Build a robot that detects, picks, and sorts colored cubes with vision and motion planning, from static poses to a Python script to an optional module." authors: [] level: "Intermediate" languages: ["python"] From 3c3e48420845e1a5197cdc6f675b1c882421dbc5 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 18:00:15 -0400 Subject: [PATCH 06/44] feat(tutorials): add structured stubs for workshop phases 1, 2, 4, 5 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0138T5LK4cgYNgn3X54V7iZq --- .../01-platform-mental-model.md | 47 ++++++++++++++++ .../pick-and-place/02-configure-resources.md | 40 ++++++++++++++ .../pick-and-place/04-local-python-script.md | 54 +++++++++++++++++++ .../pick-and-place/05-inline-module.md | 36 +++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 docs/tutorials/pick-and-place/01-platform-mental-model.md create mode 100644 docs/tutorials/pick-and-place/02-configure-resources.md create mode 100644 docs/tutorials/pick-and-place/04-local-python-script.md create mode 100644 docs/tutorials/pick-and-place/05-inline-module.md diff --git a/docs/tutorials/pick-and-place/01-platform-mental-model.md b/docs/tutorials/pick-and-place/01-platform-mental-model.md new file mode 100644 index 0000000000..3e0394e65b --- /dev/null +++ b/docs/tutorials/pick-and-place/01-platform-mental-model.md @@ -0,0 +1,47 @@ +--- +title: "Phase 1: Platform mental model" +linkTitle: "1. Platform mental model" +type: "docs" +slug: "platform-mental-model" +weight: 10 +description: "Understand how the Viam cloud, agent, and server fit together before you configure anything." +workshop: "pick-and-place" +phase: 1 +phase_total: 5 +time_estimate: "15 minutes" +next: "/tutorials/pick-and-place/configure-resources/" +languages: ["python"] +draft: true +--- + +Before you configure a single resource, this phase gives you the mental map you need to understand what happens when you make an API call, why config changes appear instantly on the robot, and how Python code on your laptop talks to hardware on the other side of the room. + +## Three questions to answer first + + + +## The three layers + + + +## How your SDK connects + + + +## Configuration is the source of truth + + + +## Resources: the universal abstraction + + + +## Components and services + + + +## The resource dependency graph + + + +{{< workshop-nav >}} diff --git a/docs/tutorials/pick-and-place/02-configure-resources.md b/docs/tutorials/pick-and-place/02-configure-resources.md new file mode 100644 index 0000000000..8f00b8a5c1 --- /dev/null +++ b/docs/tutorials/pick-and-place/02-configure-resources.md @@ -0,0 +1,40 @@ +--- +title: "Phase 2: Configure resources" +linkTitle: "2. Configure resources" +type: "docs" +slug: "configure-resources" +weight: 20 +description: "Tour the pre-configured resources, the color-detection vision pipeline, and the control and scene tabs." +workshop: "pick-and-place" +phase: 2 +phase_total: 5 +time_estimate: "20 minutes" +prev: "/tutorials/pick-and-place/platform-mental-model/" +next: "/tutorials/pick-and-place/static-positions/" +languages: ["python"] +draft: true +--- + +In this phase you open the pre-loaded machine configuration, verify that each resource is live, and get familiar with the Viam app tools you will use throughout the rest of the workshop. + +## What is already configured + + + +## The vision pipeline + + + +## Explore the control tab + + + +{{< checkpoint >}} +TODO: confirm the camera, arm, and vision test cards each return data. +{{< /checkpoint >}} + +## The 3D scene tab + + + +{{< workshop-nav >}} diff --git a/docs/tutorials/pick-and-place/04-local-python-script.md b/docs/tutorials/pick-and-place/04-local-python-script.md new file mode 100644 index 0000000000..cf93ec5ee6 --- /dev/null +++ b/docs/tutorials/pick-and-place/04-local-python-script.md @@ -0,0 +1,54 @@ +--- +title: "Phase 4: Local Python script" +linkTitle: "4. Local Python script" +type: "docs" +slug: "local-python-script" +weight: 40 +description: "Connect from your laptop, run the static sequence, then add perception to complete the pick-and-sort loop." +workshop: "pick-and-place" +phase: 4 +phase_total: 5 +time_estimate: "30 minutes" +prev: "/tutorials/pick-and-place/static-positions/" +next: "/tutorials/pick-and-place/inline-module/" +languages: ["python"] +draft: true +--- + +This is the core phase of the workshop: you write and run a Python script on your laptop that connects to the robot, executes the static pick-and-place sequence from Phase 3, and then adds live color detection to sort cubes autonomously. + +## Why a script before a module + + + +## Check your environment + + + + +## Connect to your robot + + + +## Run the static sequence + + + +## The frame system and transforms + + + +## Add perception + + + + +## Pass obstacles to the motion service + + + +## Debugging guide + + + +{{< workshop-nav >}} diff --git a/docs/tutorials/pick-and-place/05-inline-module.md b/docs/tutorials/pick-and-place/05-inline-module.md new file mode 100644 index 0000000000..91744c14d2 --- /dev/null +++ b/docs/tutorials/pick-and-place/05-inline-module.md @@ -0,0 +1,36 @@ +--- +title: "Phase 5: Inline module" +linkTitle: "5. Inline module" +type: "docs" +slug: "inline-module" +weight: 50 +description: "Optional: package your working script as an inline module that runs on the robot." +workshop: "pick-and-place" +phase: 5 +phase_total: 5 +time_estimate: "15 minutes" +prev: "/tutorials/pick-and-place/local-python-script/" +languages: ["python"] +draft: true +--- + +This phase is optional: if you want the pick-and-sort cycle to run on the robot without a laptop connection, you can package your Phase 4 script as an inline module using the Viam app's built-in editor. + +## Script versus module + + + +## The inline module editor + + + +## Dependency injection + + + + +## do_command and a scheduled job + + + +{{< workshop-nav >}} From 74910c96954f63ab876098b0a533e844fac38e22 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 18:03:53 -0400 Subject: [PATCH 07/44] docs(tutorials): clarify headless/draft removal when copying phase template --- docs/tutorials/pick-and-place/_phase-template.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/pick-and-place/_phase-template.md b/docs/tutorials/pick-and-place/_phase-template.md index 34475aa727..5c73c4d876 100644 --- a/docs/tutorials/pick-and-place/_phase-template.md +++ b/docs/tutorials/pick-and-place/_phase-template.md @@ -12,8 +12,8 @@ time_estimate: "NN minutes" prev: "/tutorials/pick-and-place/previous-slug/" next: "/tutorials/pick-and-place/next-slug/" languages: ["python"] -draft: true -headless: true +draft: true # set to false when this phase is ready to publish +headless: true # REMOVE this line when you copy the template to a real phase page (it keeps only the template itself from rendering) --- From cec95b94b94c0f97622c1bb5d9ae982a6740b50c Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 18:06:36 -0400 Subject: [PATCH 08/44] feat(tutorials): author Phase 3 (static positions) workshop exemplar Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_0138T5LK4cgYNgn3X54V7iZq --- .../pick-and-place/03-static-positions.md | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 docs/tutorials/pick-and-place/03-static-positions.md diff --git a/docs/tutorials/pick-and-place/03-static-positions.md b/docs/tutorials/pick-and-place/03-static-positions.md new file mode 100644 index 0000000000..55d96f31b9 --- /dev/null +++ b/docs/tutorials/pick-and-place/03-static-positions.md @@ -0,0 +1,145 @@ +--- +title: "Phase 3: Static positions and safety obstacles" +linkTitle: "3. Static positions" +type: "docs" +slug: "static-positions" +weight: 30 +description: "Save the arm's key poses and configure WorldState obstacles, proving the hardware and motion planning work before you add perception." +workshop: "pick-and-place" +phase: 3 +phase_total: 5 +time_estimate: "20 minutes" +prev: "/tutorials/pick-and-place/configure-resources/" +next: "/tutorials/pick-and-place/local-python-script/" +languages: ["python"] +draft: true +--- + + + +In this phase you move the arm through the full pick-and-place sequence using saved poses, with no perception yet. The goal is to prove that the hardware and motion planning work before you add detection, so that any bug you encounter later has only one new cause. + +## 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. Once the arm reliably travels through every stage of the sequence, perception becomes the only new variable when you move to Phase 4. + +The table below shows what each step in the static sequence validates: + +| Step | What it validates | +|------|-------------------| +| Arm reaches home pose | Observation position is safe and repeatable | +| Arm reaches approach pose | Arm can get above the workspace without collision | +| Arm reaches grasp pose | Descent distance is correct | +| Gripper opens and closes | Finger timing is right | +| Arm reaches travel pose | Safe carrying height clears obstacles | +| Arm reaches place pose | Bin position is correct | + +## The key poses + +Each saved pose has a specific role in the sequence: + +| Pose | Purpose | +|------|---------| +| home-pose | Observation position above the workspace; the camera has a clear view of all cubes | +| approach-pose | Directly above the pick zone, roughly 80 to 100 mm above the highest cube | +| grasp-pose | At the cube with the gripper open; fingertips are level with the cube top | +| travel-pose | Safe carrying height that clears bins and table edges while holding a cube | +| [color]-bin-pose | Above each target sorting bin; one pose per color (red, blue, green) | + +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. + +## Save each pose with the arm position saver + +The machine is already configured with `arm-position-saver` switch components (model `erh:vmodutils:arm-position-saver`), one per pose. Each switch has an `arm` attribute pointing to `arm-1`. If you are setting up your own machine rather than using the pre-loaded configuration, add the `erh:vmodutils` module from the Viam registry and add a switch component for each pose. + +For each pose, follow these steps: + +1. Open the **CONTROL** tab and find the arm test card. Use the joint sliders to jog the arm into position. +2. Select **Get end position** on the arm card and note the x, y, and z values to confirm the arm is where you expect it. +3. On the pose's switch test card, set the switch to **position 1** to save the current joint positions. +4. Set the switch to **position 2** to confirm the arm returns to the saved pose from any starting position. + +Repeat this process for home-pose, approach-pose, grasp-pose, travel-pose, red-bin-pose, blue-bin-pose, and green-bin-pose. + +{{< alert title="Switch positions" color="note" >}} +On an `arm-position-saver` switch, position 1 saves the current joint positions, position 2 moves the arm to the saved pose, and position 0 clears any saved data. Always save with position 1 before you attempt position 2. Setting position 2 on an unsaved switch does nothing. +{{< /alert >}} + +{{< checkpoint >}} +Set each saved switch to position 2 in turn. The arm should move to each pose you saved. 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. +{{< /checkpoint >}} + +## Teach the planner about obstacles with WorldState + +The Viam motion planner is collision-aware, but it can only avoid geometry it knows about. Without a WorldState configuration, the planner avoids self-collisions only. When you add WorldState, the planner treats the table surface, the sorting bins, and any workspace boundary walls as hard obstacles it cannot plan through. + +This matters both for correctness and for safety. Without the table obstacle, the planner might find a path that swings the arm through the table surface. Without bin obstacles, it might carry a cube directly through a bin wall. Virtual obstacle walls at the workspace boundary also prevent the arm from swinging into people standing nearby. + +For this workshop you define three categories of obstacle: the table surface, one box per sorting bin, and safety walls at the workspace boundary. + +## Measure and configure the obstacles + +Your workshop facilitator provides the table and bin dimensions. To find each bin's position relative to the arm base, move the arm over the center of the bin and read the x and y values from **Get end position**. The z coordinate for a box obstacle is the center of the box, not its top surface. + +The following JSON shows the structure for the obstacles array. Add this to the WorldState configuration for your motion service: + +```json +{ + "obstacles": [ + { + "label": "table", + "geometries": [ + { + "type": "box", + "x": 1200, + "y": 800, + "z": 30, + "translation": { + "x": 0, + "y": 0, + "z": -15 + } + } + ] + }, + { + "label": "red-bin", + "geometries": [ + { + "type": "box", + "x": 200, + "y": 200, + "z": 150, + "translation": { + "x": "[measured]", + "y": "[measured]", + "z": 75 + } + } + ] + } + ] +} +``` + +The table z translation is -15 because the box is 30 mm thick and its center sits 15 mm below the world origin at z = 0. Each bin's z translation is half its height because the box center is at the midpoint of the bin walls. Replace `[measured]` with the x and y values you read from the arm when centered over each bin, then add a matching entry for blue-bin and green-bin. + +You can import a ready-made starting point from the companion repo: [obstacles-template.json](https://github.com/viam-devrel/pick-and-place/blob/main/config/obstacles-template.json). The full machine configuration, including all pose switches, is in [machine-fragment.json](https://github.com/viam-devrel/pick-and-place/blob/main/config/machine-fragment.json). + +## Test the full static sequence + +From the **CONTROL** tab, trigger the pose switches in this order: + +```text +home-pose (2) -> approach-pose (2) -> open gripper -> +grasp-pose (2) -> close gripper -> travel-pose (2) -> +red-bin-pose (2) -> open gripper -> home-pose (2) +``` + +As the arm moves, open the **3D scene** tab and watch the arm's path to confirm it does not intersect the table surface or bin walls. If the path clips an obstacle boundary, the planner will report a collision error. Open the **LOGS** tab alongside the 3D scene to monitor for motion planning errors in real time. + +{{< checkpoint >}} +The arm completes the full sequence without stopping, the gripper opens and closes at the correct moments, and the LOGS tab shows no collision errors. If planning fails at a step, open the 3D scene tab to see what geometry the planner sees. Adjust the pose or the obstacle dimensions and retry. A common cause is a bin obstacle that is positioned slightly off center, causing the planner to see the arm path as intersecting the bin wall even though the physical arm clears it. +{{< /checkpoint >}} + +{{< workshop-nav >}} From 918fce728d1a32a8c045d78d9afe58ba50de316c Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 18:08:41 -0400 Subject: [PATCH 09/44] feat(tutorials): author Phase 3 (static positions) workshop exemplar --- docs/tutorials/pick-and-place/03-static-positions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/pick-and-place/03-static-positions.md b/docs/tutorials/pick-and-place/03-static-positions.md index 55d96f31b9..f34a71c737 100644 --- a/docs/tutorials/pick-and-place/03-static-positions.md +++ b/docs/tutorials/pick-and-place/03-static-positions.md @@ -1,5 +1,5 @@ --- -title: "Phase 3: Static positions and safety obstacles" +title: "Phase 3: Static positions and obstacles" linkTitle: "3. Static positions" type: "docs" slug: "static-positions" @@ -83,6 +83,8 @@ Your workshop facilitator provides the table and bin dimensions. To find each bi The following JSON shows the structure for the obstacles array. Add this to the WorldState configuration for your motion service: + + ```json { "obstacles": [ From 1b5be959271cea7905f9df74f7562feb241328f5 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 18:11:44 -0400 Subject: [PATCH 10/44] style(tutorials): prettier-format workshop pages; fix Phase 3 pose-name consistency --- .../pick-and-place/03-static-positions.md | 28 +++++++++---------- docs/tutorials/pick-and-place/_index.md | 3 +- .../pick-and-place/_phase-template.md | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/tutorials/pick-and-place/03-static-positions.md b/docs/tutorials/pick-and-place/03-static-positions.md index f34a71c737..c3d5d787c8 100644 --- a/docs/tutorials/pick-and-place/03-static-positions.md +++ b/docs/tutorials/pick-and-place/03-static-positions.md @@ -25,26 +25,26 @@ When you add perception and motion planning at the same time, a failure could li The table below shows what each step in the static sequence validates: -| Step | What it validates | -|------|-------------------| -| Arm reaches home pose | Observation position is safe and repeatable | +| Step | What it validates | +| ------------------------- | ------------------------------------------------- | +| Arm reaches home pose | Observation position is safe and repeatable | | Arm reaches approach pose | Arm can get above the workspace without collision | -| Arm reaches grasp pose | Descent distance is correct | -| Gripper opens and closes | Finger timing is right | -| Arm reaches travel pose | Safe carrying height clears obstacles | -| Arm reaches place pose | Bin position is correct | +| Arm reaches grasp pose | Descent distance is correct | +| Gripper opens and closes | Finger timing is right | +| Arm reaches travel pose | Safe carrying height clears obstacles | +| Arm reaches bin pose | Bin position is correct | ## The key poses Each saved pose has a specific role in the sequence: -| Pose | Purpose | -|------|---------| -| home-pose | Observation position above the workspace; the camera has a clear view of all cubes | -| approach-pose | Directly above the pick zone, roughly 80 to 100 mm above the highest cube | -| grasp-pose | At the cube with the gripper open; fingertips are level with the cube top | -| travel-pose | Safe carrying height that clears bins and table edges while holding a cube | -| [color]-bin-pose | Above each target sorting bin; one pose per color (red, blue, green) | +| Pose | Purpose | +| ---------------- | ---------------------------------------------------------------------------------- | +| home-pose | Observation position above the workspace; the camera has a clear view of all cubes | +| approach-pose | Directly above the pick zone, roughly 80 to 100 mm above the highest cube | +| grasp-pose | At the cube with the gripper open; fingertips are level with the cube top | +| travel-pose | Safe carrying height that clears bins and table edges while holding a cube | +| [color]-bin-pose | Above each target sorting bin; one pose per color (red, blue, green) | 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. diff --git a/docs/tutorials/pick-and-place/_index.md b/docs/tutorials/pick-and-place/_index.md index fa69eda2e5..88cbfcfb05 100644 --- a/docs/tutorials/pick-and-place/_index.md +++ b/docs/tutorials/pick-and-place/_index.md @@ -7,7 +7,8 @@ description: "Build a robot that detects, picks, and sorts colored cubes with vi authors: [] level: "Intermediate" languages: ["python"] -viamresources: ["arm", "gripper", "camera", "vision", "motion", "frame_system", "switch"] +viamresources: + ["arm", "gripper", "camera", "vision", "motion", "frame_system", "switch"] platformarea: ["mobility", "core"] tags: ["tutorial", "workshop", "arm", "vision", "motion"] workshop: "pick-and-place" diff --git a/docs/tutorials/pick-and-place/_phase-template.md b/docs/tutorials/pick-and-place/_phase-template.md index 5c73c4d876..6d2c552b49 100644 --- a/docs/tutorials/pick-and-place/_phase-template.md +++ b/docs/tutorials/pick-and-place/_phase-template.md @@ -3,7 +3,7 @@ title: "Phase N: Title in under 70 characters" linkTitle: "N. Short title" type: "docs" slug: "phase-slug" -weight: 0 # 10, 20, 30, ... in 10s for sidebar order +weight: 0 # 10, 20, 30, ... in 10s for sidebar order description: "One-sentence description of this phase." workshop: "pick-and-place" phase: 0 From 38ce71060ce5385e543f953214987536e6e85940 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 18:13:23 -0400 Subject: [PATCH 11/44] feat(tutorials): show one landing card per workshop, exclude phase pages --- layouts/docs/tutorials.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/layouts/docs/tutorials.html b/layouts/docs/tutorials.html index 7477b2717f..b06dc2f6c4 100644 --- a/layouts/docs/tutorials.html +++ b/layouts/docs/tutorials.html @@ -62,8 +62,18 @@

Missing the filtering UI?

{{ $pctx := .Site }} {{- $pages := (where (where $pctx.RegularPages ".Section" "tutorials") "Kind" "page") -}} + {{/* Exclude sequential-workshop phase pages; each workshop + surfaces via a single overview card below. */}} + {{- $phasePages := where $pages "Params.workshop" "!=" nil -}} + {{- $pages = complement $phasePages $pages -}} + {{/* One card per workshop overview (the workshop's _index.md). */}} + {{- $workshops := where $pctx.Pages "Params.workshop_overview" true + -}}
+ {{ range $workshops }} {{ partial "card.html" (dict "link" + (.File.Dir) "class" "" "customTitle" "" "customDescription" "" + "customCanonicalLink" "") }} {{ end }} {{ range sort $pages ".Weight"}} {{ partial "card.html" (dict "link" (.Page.File.Path) "class" "" "customTitle" "" "customDescription" "" "customCanonicalLink" From 2e40921691cfc4536337b6bb5fe17778df424d0f Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 18:13:23 -0400 Subject: [PATCH 12/44] chore: ignore pending pick-and-place companion repo in htmltest --- .htmltest.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.htmltest.yml b/.htmltest.yml index aaac581364..9433ed7680 100644 --- a/.htmltest.yml +++ b/.htmltest.yml @@ -26,6 +26,7 @@ IgnoreURLs: - "instagram.com" - "twitter.com" - "github.com/viamrobotics/docs" + - "github.com/viam-devrel/pick-and-place" - "openai.com" - "espressif.com" - "pinout.xyz" From f126936c552ba886151ae0192eee6818ea2287d3 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Wed, 24 Jun 2026 18:17:06 -0400 Subject: [PATCH 13/44] refactor(tutorials): use .Path for workshop card link (override-proof) --- layouts/docs/tutorials.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layouts/docs/tutorials.html b/layouts/docs/tutorials.html index b06dc2f6c4..990f9829cd 100644 --- a/layouts/docs/tutorials.html +++ b/layouts/docs/tutorials.html @@ -72,7 +72,7 @@

Missing the filtering UI?

{{ range $workshops }} {{ partial "card.html" (dict "link" - (.File.Dir) "class" "" "customTitle" "" "customDescription" "" + (.Path) "class" "" "customTitle" "" "customDescription" "" "customCanonicalLink" "") }} {{ end }} {{ range sort $pages ".Weight"}} {{ partial "card.html" (dict "link" (.Page.File.Path) "class" "" "customTitle" "" From 4e9cfe83071066822cb3e3bf34d8551f86928448 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Fri, 26 Jun 2026 09:15:48 -0400 Subject: [PATCH 14/44] feat(tutorials): add in-page workshop phase list for cross-phase navigation The tutorials sidebar is a minimal, per-section-cached skeleton that does not render a section tree, so workshop phases were not navigable from phase pages. Add a workshop-phases shortcode (full phase list, current highlighted) at the top of each phase page and the template. --- .../01-platform-mental-model.md | 2 + .../pick-and-place/02-configure-resources.md | 2 + .../pick-and-place/03-static-positions.md | 2 + .../pick-and-place/04-local-python-script.md | 2 + .../pick-and-place/05-inline-module.md | 2 + .../pick-and-place/_phase-template.md | 2 + layouts/shortcodes/workshop-phases.html | 40 +++++++++++++++++++ 7 files changed, 52 insertions(+) create mode 100644 layouts/shortcodes/workshop-phases.html diff --git a/docs/tutorials/pick-and-place/01-platform-mental-model.md b/docs/tutorials/pick-and-place/01-platform-mental-model.md index 3e0394e65b..3bfe18d529 100644 --- a/docs/tutorials/pick-and-place/01-platform-mental-model.md +++ b/docs/tutorials/pick-and-place/01-platform-mental-model.md @@ -16,6 +16,8 @@ draft: true Before you configure a single resource, this phase gives you the mental map you need to understand what happens when you make an API call, why config changes appear instantly on the robot, and how Python code on your laptop talks to hardware on the other side of the room. +{{< workshop-phases >}} + ## Three questions to answer first diff --git a/docs/tutorials/pick-and-place/02-configure-resources.md b/docs/tutorials/pick-and-place/02-configure-resources.md index 8f00b8a5c1..19b24412c2 100644 --- a/docs/tutorials/pick-and-place/02-configure-resources.md +++ b/docs/tutorials/pick-and-place/02-configure-resources.md @@ -17,6 +17,8 @@ draft: true In this phase you open the pre-loaded machine configuration, verify that each resource is live, and get familiar with the Viam app tools you will use throughout the rest of the workshop. +{{< workshop-phases >}} + ## What is already configured diff --git a/docs/tutorials/pick-and-place/03-static-positions.md b/docs/tutorials/pick-and-place/03-static-positions.md index c3d5d787c8..881970ba5b 100644 --- a/docs/tutorials/pick-and-place/03-static-positions.md +++ b/docs/tutorials/pick-and-place/03-static-positions.md @@ -19,6 +19,8 @@ draft: true In this phase you move the arm through the full pick-and-place sequence using saved poses, with no perception yet. The goal is to prove that the hardware and motion planning work before you add detection, so that any bug you encounter later has only one new cause. +{{< workshop-phases >}} + ## 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. Once the arm reliably travels through every stage of the sequence, perception becomes the only new variable when you move to Phase 4. diff --git a/docs/tutorials/pick-and-place/04-local-python-script.md b/docs/tutorials/pick-and-place/04-local-python-script.md index cf93ec5ee6..5a08600d16 100644 --- a/docs/tutorials/pick-and-place/04-local-python-script.md +++ b/docs/tutorials/pick-and-place/04-local-python-script.md @@ -17,6 +17,8 @@ draft: true This is the core phase of the workshop: you write and run a Python script on your laptop that connects to the robot, executes the static pick-and-place sequence from Phase 3, and then adds live color detection to sort cubes autonomously. +{{< workshop-phases >}} + ## Why a script before a module diff --git a/docs/tutorials/pick-and-place/05-inline-module.md b/docs/tutorials/pick-and-place/05-inline-module.md index 91744c14d2..3767984f94 100644 --- a/docs/tutorials/pick-and-place/05-inline-module.md +++ b/docs/tutorials/pick-and-place/05-inline-module.md @@ -16,6 +16,8 @@ draft: true This phase is optional: if you want the pick-and-sort cycle to run on the robot without a laptop connection, you can package your Phase 4 script as an inline module using the Viam app's built-in editor. +{{< workshop-phases >}} + ## Script versus module diff --git a/docs/tutorials/pick-and-place/_phase-template.md b/docs/tutorials/pick-and-place/_phase-template.md index 6d2c552b49..ca6cd63cff 100644 --- a/docs/tutorials/pick-and-place/_phase-template.md +++ b/docs/tutorials/pick-and-place/_phase-template.md @@ -18,6 +18,8 @@ headless: true # REMOVE this line when you copy the template to a real phase pag +{{< workshop-phases >}} + ## Section heading diff --git a/layouts/shortcodes/workshop-phases.html b/layouts/shortcodes/workshop-phases.html new file mode 100644 index 0000000000..3502911741 --- /dev/null +++ b/layouts/shortcodes/workshop-phases.html @@ -0,0 +1,40 @@ +{{/* workshop-phases.html + In-page phase list for a sequential workshop, with the current page + highlighted. Provides cross-phase navigation because the tutorials sidebar + is a minimal, cached skeleton and does not render a section tree. + Derives the phases from the current section's regular pages, ordered by + weight. CSS is injected once per page via a Scratch guard (like notice.html). */}} +{{- $cur := .Page -}} +{{- $section := .Page.CurrentSection -}} +{{- $phases := $section.RegularPages.ByWeight -}} +{{- if not ($.Page.Scratch.Get "workshopphasescss") -}} + +{{- $.Page.Scratch.Set "workshopphasescss" 1 -}} +{{- end -}} + From 66cdf103ae93189f6985ab5a1330af140a51977f Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Fri, 26 Jun 2026 09:27:59 -0400 Subject: [PATCH 15/44] feat(tutorials): publish pick-and-place workshop pages (remove draft) Take the overview and five phase pages out of Hugo draft mode so they render in the Netlify deploy preview (which builds without --buildDrafts). The headless _phase-template.md stays draft. Note: phase-page exclusion from the production Typesense card grid remains a tracked follow-up. --- docs/tutorials/pick-and-place/01-platform-mental-model.md | 1 - docs/tutorials/pick-and-place/02-configure-resources.md | 1 - docs/tutorials/pick-and-place/03-static-positions.md | 1 - docs/tutorials/pick-and-place/04-local-python-script.md | 1 - docs/tutorials/pick-and-place/05-inline-module.md | 1 - docs/tutorials/pick-and-place/_index.md | 1 - 6 files changed, 6 deletions(-) diff --git a/docs/tutorials/pick-and-place/01-platform-mental-model.md b/docs/tutorials/pick-and-place/01-platform-mental-model.md index 3bfe18d529..5c717c679d 100644 --- a/docs/tutorials/pick-and-place/01-platform-mental-model.md +++ b/docs/tutorials/pick-and-place/01-platform-mental-model.md @@ -11,7 +11,6 @@ phase_total: 5 time_estimate: "15 minutes" next: "/tutorials/pick-and-place/configure-resources/" languages: ["python"] -draft: true --- Before you configure a single resource, this phase gives you the mental map you need to understand what happens when you make an API call, why config changes appear instantly on the robot, and how Python code on your laptop talks to hardware on the other side of the room. diff --git a/docs/tutorials/pick-and-place/02-configure-resources.md b/docs/tutorials/pick-and-place/02-configure-resources.md index 19b24412c2..4ac7c8f7ff 100644 --- a/docs/tutorials/pick-and-place/02-configure-resources.md +++ b/docs/tutorials/pick-and-place/02-configure-resources.md @@ -12,7 +12,6 @@ time_estimate: "20 minutes" prev: "/tutorials/pick-and-place/platform-mental-model/" next: "/tutorials/pick-and-place/static-positions/" languages: ["python"] -draft: true --- In this phase you open the pre-loaded machine configuration, verify that each resource is live, and get familiar with the Viam app tools you will use throughout the rest of the workshop. diff --git a/docs/tutorials/pick-and-place/03-static-positions.md b/docs/tutorials/pick-and-place/03-static-positions.md index 881970ba5b..b07c9e96d8 100644 --- a/docs/tutorials/pick-and-place/03-static-positions.md +++ b/docs/tutorials/pick-and-place/03-static-positions.md @@ -12,7 +12,6 @@ time_estimate: "20 minutes" prev: "/tutorials/pick-and-place/configure-resources/" next: "/tutorials/pick-and-place/local-python-script/" languages: ["python"] -draft: true --- diff --git a/docs/tutorials/pick-and-place/04-local-python-script.md b/docs/tutorials/pick-and-place/04-local-python-script.md index 5a08600d16..90ad1ea306 100644 --- a/docs/tutorials/pick-and-place/04-local-python-script.md +++ b/docs/tutorials/pick-and-place/04-local-python-script.md @@ -12,7 +12,6 @@ time_estimate: "30 minutes" prev: "/tutorials/pick-and-place/static-positions/" next: "/tutorials/pick-and-place/inline-module/" languages: ["python"] -draft: true --- This is the core phase of the workshop: you write and run a Python script on your laptop that connects to the robot, executes the static pick-and-place sequence from Phase 3, and then adds live color detection to sort cubes autonomously. diff --git a/docs/tutorials/pick-and-place/05-inline-module.md b/docs/tutorials/pick-and-place/05-inline-module.md index 3767984f94..0e47974aef 100644 --- a/docs/tutorials/pick-and-place/05-inline-module.md +++ b/docs/tutorials/pick-and-place/05-inline-module.md @@ -11,7 +11,6 @@ phase_total: 5 time_estimate: "15 minutes" prev: "/tutorials/pick-and-place/local-python-script/" languages: ["python"] -draft: true --- This phase is optional: if you want the pick-and-sort cycle to run on the robot without a laptop connection, you can package your Phase 4 script as an inline module using the Viam app's built-in editor. diff --git a/docs/tutorials/pick-and-place/_index.md b/docs/tutorials/pick-and-place/_index.md index 88cbfcfb05..107c6b370b 100644 --- a/docs/tutorials/pick-and-place/_index.md +++ b/docs/tutorials/pick-and-place/_index.md @@ -21,7 +21,6 @@ hardware: - "System76 Meerkat" companion_repo: "https://github.com/viam-devrel/pick-and-place" no_list: true -draft: true --- In this workshop you build a vision-guided robot that detects colored cubes, picks each one up, and drops it in the correct bin, sorted by color. The workshop is structured as five sequential phases, each ending with a working system state you can verify before moving on. Completing Phase 4 (the local Python script) is a full success; Phase 5 (packaging your script as an inline module) is optional. From eb92fd77caeb4db0210c87a1c4535acc71881043 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Fri, 26 Jun 2026 09:36:34 -0400 Subject: [PATCH 16/44] fix(tutorials): link phases from the workshop overview The overview Phases list and the Prerequisites Phase 1 references were plain text, so there was no way to start the workshop from the overview. Link each phase to its page. --- docs/tutorials/pick-and-place/_index.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/pick-and-place/_index.md b/docs/tutorials/pick-and-place/_index.md index 107c6b370b..daaf1c65ca 100644 --- a/docs/tutorials/pick-and-place/_index.md +++ b/docs/tutorials/pick-and-place/_index.md @@ -40,17 +40,17 @@ You will configure an xArm6 robotic arm fitted with a finger gripper and an Inte ## Phases -1. **Platform mental model** (~15 min): learn how Viam resources, the frame system, and the motion service fit together before touching any hardware. -2. **Configure resources and explore the app** (~20 min): add the arm, gripper, camera, and vision service in the Viam app; confirm each resource is live. -3. **Static positions and safety obstacles** (~20 min): define named arm poses and add a virtual obstacle to prevent the arm from hitting the table. -4. **Local Python script** (~30 min): write and run the pick-and-sort script from your laptop; this is the core goal of the workshop. -5. **Inline module** (~15 min, optional): package the script as an on-robot module so the cycle runs without a laptop connection. +1. **[Platform mental model](/tutorials/pick-and-place/platform-mental-model/)** (~15 min): learn how Viam resources, the frame system, and the motion service fit together before touching any hardware. +2. **[Configure resources and explore the app](/tutorials/pick-and-place/configure-resources/)** (~20 min): add the arm, gripper, camera, and vision service in the Viam app; confirm each resource is live. +3. **[Static positions and safety obstacles](/tutorials/pick-and-place/static-positions/)** (~20 min): define named arm poses and add a virtual obstacle to prevent the arm from hitting the table. +4. **[Local Python script](/tutorials/pick-and-place/local-python-script/)** (~30 min): write and run the pick-and-sort script from your laptop; this is the core goal of the workshop. +5. **[Inline module](/tutorials/pick-and-place/inline-module/)** (~15 min, optional): package the script as an on-robot module so the cycle runs without a laptop connection. ## Prerequisites -**Hardware pre-provisioned for you:** if you are in a guided workshop where the hardware is already set up, skip directly to Phase 1. +**Hardware pre-provisioned for you:** if you are in a guided workshop where the hardware is already set up, skip directly to [Phase 1](/tutorials/pick-and-place/platform-mental-model/). -**Provisioning your own hardware:** complete the hardware setup guide first (forthcoming), then return here for Phase 1. The setup guide covers mounting the camera, connecting the arm controller, and installing viam-server on the Meerkat. +**Provisioning your own hardware:** complete the hardware setup guide first (forthcoming), then return here for [Phase 1](/tutorials/pick-and-place/platform-mental-model/). The setup guide covers mounting the camera, connecting the arm controller, and installing viam-server on the Meerkat. Before Phase 4 you also need Python 3.10 or newer and the Viam Python SDK on the machine you will run the script from. Verify with: From 31c368007df2590d31f88d8a4bf125ebee44f745 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Fri, 26 Jun 2026 10:49:40 -0400 Subject: [PATCH 17/44] feat(tutorials): surface Tutorials in top navbar via menu.main Populate the previously-empty Site.Menus.main by adding a menu.main entry to the tutorials section front matter. Hugo associates the entry with the page, so the navbar resolves the href to /tutorials/ and applies the active class on every /tutorials/... page. The sidebar (toc_hide) and footer are unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0138T5LK4cgYNgn3X54V7iZq --- docs/tutorials/_index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/tutorials/_index.md b/docs/tutorials/_index.md index e2a79a4651..89413c3710 100644 --- a/docs/tutorials/_index.md +++ b/docs/tutorials/_index.md @@ -10,6 +10,9 @@ toc_hide: true # do not remove hide children - it causes a layout issue no_list: true hide_children: true +menu: + main: + weight: 10 sitemap: priority: 1.0 outputs: From ea56edf0c4542550014e2915fc012ecd3222a2b6 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 10:33:53 -0400 Subject: [PATCH 18/44] feat(tutorials): add /tutorials/all/ archive with full filter UI Creates docs/tutorials/all/_index.md and layouts/docs/tutorials-all.html so the Typesense filter UI and dev archive fallback are available at the new /tutorials/all/ URL, without touching the existing /tutorials/ landing. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- docs/tutorials/all/_index.md | 12 ++ layouts/docs/tutorials-all.html | 250 ++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 docs/tutorials/all/_index.md create mode 100644 layouts/docs/tutorials-all.html diff --git a/docs/tutorials/all/_index.md b/docs/tutorials/all/_index.md new file mode 100644 index 0000000000..abc18d0c5b --- /dev/null +++ b/docs/tutorials/all/_index.md @@ -0,0 +1,12 @@ +--- +title: "All Tutorials" +linkTitle: "All Tutorials" +type: "docs" +layout: "tutorials-all" +weight: 1 +toc_hide: true +no_list: true +hide_children: true +outputs: + - html +--- diff --git a/layouts/docs/tutorials-all.html b/layouts/docs/tutorials-all.html new file mode 100644 index 0000000000..529c0bb2c3 --- /dev/null +++ b/layouts/docs/tutorials-all.html @@ -0,0 +1,250 @@ + + + + {{ partial "head.html" . }} + + + {{- if hugo.IsProduction -}}{{- if .Site.Config.Services.GoogleAnalytics.ID + -}} + + + + {{- end -}}{{- end -}} +
{{ partial "navbar.html" . }}
+
+
+
+ +
+ + {{ if not .Site.Params.ui.breadcrumb_disable }}{{ partial + "breadcrumb.html" . }}{{ end }} +
+

{{ .Title }}

+

Browse every Viam tutorial. Filter by language, component, service, level, or content type.

+ {{ $pctx := .Site }} + {{ if not hugo.IsProduction }} + + {{- $pages := (where (where $pctx.RegularPages ".Section" "tutorials") "Kind" "page") -}} + {{/* Exclude sequential-workshop phase pages; each workshop surfaces via its overview card above. */}} + {{- $phasePages := where $pages "Params.workshop" "!=" nil -}} + {{- $pages = complement $phasePages $pages -}} +
+
+ {{ range sort $pages ".Weight"}} {{ partial "card.html" (dict "link" (.Page.File.Path) "class" "" "customTitle" "" "customDescription" "" "customCanonicalLink" (.Page.Params.canonical) ) }} {{ end }} +
+
+ {{ else }} +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+ {{ end }} + + +
+

+ Find more examples of how Viam is being used in the world by + reviewing + Customer Stories + or + blog posts. +

+ + + + + {{ partial "footer.html" . "tutorials" }} +
+
+
+
+ + {{ partial "scripts.html" . }} + + + + + + {{ $jsTutorials := resources.Get "js/tutorials.js" }} {{ if (eq (substr + .Site.BaseURL -1) "/" ) }} {{- $opts := dict "params" (dict "baseURL" + (substr .Site.BaseURL 0 -1 )) -}} {{- $jsTutorials = $jsTutorials | js.Build + $opts -}} {{ $jsTutorials := $jsTutorials | minify }} + + {{ else }} {{- $opts := dict "params" (dict "baseURL" .Site.BaseURL) -}} {{- + $jsTutorials = $jsTutorials | js.Build $opts -}} {{ $jsTutorials := + $jsTutorials | minify }} + + {{ end }} + + From fb235abed0ab0e6edacb6d9950d1384f0c6eb0c4 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 10:37:19 -0400 Subject: [PATCH 19/44] fix(navbar): make Tutorials link legible, align it, and center search Resolve the Tutorials top-nav item rendering white-on-white by raising the project rule specificity above Bootstrap's .navbar-dark nav-link color (covers base, active, hover, focus). Align Tutorials next to the brand with a separator matched to the brand divider height, absolutely center the search input in the navbar, and remove the nav-link hover state to match the static brand link. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- assets/scss/_styles_project.scss | 35 ++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index e6c2f10ce9..779c170137 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -1431,6 +1431,10 @@ td > ol { } #navSearchBar { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); margin: 0px; min-width: 300px; max-width: 300px; @@ -3180,22 +3184,26 @@ nav { padding: 0.25rem 1rem; min-height: 50px; background-color: white; + position: relative; } .td-topbar-sections { margin-right: auto !important; - margin-left: auto !important; + margin-left: 0 !important; line-height: unset !important; } .td-topbar-sections > ul { + display: flex; + flex-direction: row; + align-items: center; padding: 0; - padding-left: 1.25rem; margin: 0; } .td-topbar-sections > ul > li { - display: inline-block; + display: flex; + align-items: center; text-transform: uppercase; font-family: Roboto Mono Variable, @@ -3206,19 +3214,28 @@ nav { line-height: 1.25rem; } -.td-topbar-sections > ul > li > a { +/* "|" separator before each topbar link; fixed height matches the brand divider */ +.td-topbar-sections > ul > li::before { + content: ""; + width: 1px; + height: 20px; + background-color: #e4e4e6; + margin: 0 0.5rem; +} + +/* No distinct hover/focus state — matches the brand link, which also stays static */ +.td-navbar .td-topbar-sections > ul > li > .nav-link, +.td-navbar .td-topbar-sections > ul > li > .nav-link.active, +.td-navbar .td-topbar-sections > ul > li > .nav-link:hover, +.td-navbar .td-topbar-sections > ul > li > .nav-link:focus { color: black; + text-decoration: none; padding-bottom: 4px; padding-top: 8px; padding-right: 0.75rem; padding-left: 0.75rem; } -.td-topbar-sections > ul > li > a:hover { - text-decoration: none; - background-color: rgb(232, 232, 234); -} - @media (min-width: 768px) { .td-main main { padding-top: 5rem; From 2b3289d977da00c37519e3bd773ad2e427fc553a Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 10:41:17 -0400 Subject: [PATCH 20/44] fix(tutorials): drop dead RSS link and tidy comment on archive page Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- layouts/docs/tutorials-all.html | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/layouts/docs/tutorials-all.html b/layouts/docs/tutorials-all.html index 529c0bb2c3..12487b9349 100644 --- a/layouts/docs/tutorials-all.html +++ b/layouts/docs/tutorials-all.html @@ -27,23 +27,13 @@ {{ partial "sidebar.html" . }}
- {{ if not .Site.Params.ui.breadcrumb_disable }}{{ partial "breadcrumb.html" . }}{{ end }}

{{ .Title }}

Browse every Viam tutorial. Filter by language, component, service, level, or content type.

- {{ $pctx := .Site }} {{ if not hugo.IsProduction }} + {{ $pctx := .Site }} {{- $pages := (where (where $pctx.RegularPages ".Section" "tutorials") "Kind" "page") -}} - {{/* Exclude sequential-workshop phase pages; each workshop surfaces via its overview card above. */}} + {{/* Full-catalog archive fallback for local/staging builds where Typesense is unavailable. Exclude sequential-workshop phase pages; they surface through their workshop overview on the main landing. */}} {{- $phasePages := where $pages "Params.workshop" "!=" nil -}} {{- $pages = complement $phasePages $pages -}}
From f9c76d326c17d80b38c79cc420853d37749c7e68 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 10:43:58 -0400 Subject: [PATCH 21/44] feat(tutorials): add featured tutorial-card partial with metadata badges Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- assets/scss/_styles_project.scss | 49 +++++++++++++++++++++++++++++ layouts/partials/tutorial-card.html | 18 +++++++++++ 2 files changed, 67 insertions(+) create mode 100644 layouts/partials/tutorial-card.html diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index 779c170137..32817d20c9 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -4196,3 +4196,52 @@ footer { } /* API overview grid END */ + +/* Featured tutorial cards (tutorials landing) */ +.tutorial-card a { + display: block; + height: 100%; + text-decoration: none; + color: inherit; +} +.tutorial-card__body { + padding: 20px 22px; +} +.tutorial-card__title { + font-weight: 700; + font-size: 1.15rem; + line-height: 1.3; + margin-bottom: 8px; +} +.tutorial-card__desc { + color: #4a4a4f; + margin: 0 0 14px; +} +.tutorial-card__badges { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.tutorial-card__badges .badge { + display: inline-block; + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + padding: 3px 8px; + border-radius: 4px; + background: #f0f0f2; + color: #282829; +} +.tutorial-card__badges .badge--workshop { + background: #282829; + color: #fff; +} +.tutorial-card__badges .badge--lang { + background: #e6effa; + color: #1b4f8a; +} +.browse-all-tutorials { + margin-top: 24px; + font-weight: 600; +} diff --git a/layouts/partials/tutorial-card.html b/layouts/partials/tutorial-card.html new file mode 100644 index 0000000000..15237b1b27 --- /dev/null +++ b/layouts/partials/tutorial-card.html @@ -0,0 +1,18 @@ +{{- $p := .page -}} +{{- $isWorkshop := $p.Params.workshop_overview -}} +{{- $phaseCount := cond $isWorkshop (len $p.RegularPages) 0 -}} + From 9329b9c296083d8417c4300a0a00137e0003dd8f Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 10:44:49 -0400 Subject: [PATCH 22/44] feat(tutorials): curate landing to featured + workshop cards with archive link Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- layouts/docs/tutorials.html | 194 ++++-------------------------------- 1 file changed, 18 insertions(+), 176 deletions(-) diff --git a/layouts/docs/tutorials.html b/layouts/docs/tutorials.html index 990f9829cd..50d048b9d4 100644 --- a/layouts/docs/tutorials.html +++ b/layouts/docs/tutorials.html @@ -42,164 +42,29 @@

{{ .Title }}

- Tutorials provide instructions to build small projects while - teaching you new skills. + Hands-on guides that walk you through building a working project with Viam, + step by step.

-

- Select a language, component, or service that you're interested - in and browse relevant tutorials. If you don't know where to - start, start by selecting the beginner level for introductory - tutorials. -

- {{ if not hugo.IsProduction }} - - {{ $pctx := .Site }} {{- $pages := (where (where - $pctx.RegularPages ".Section" "tutorials") "Kind" "page") -}} - {{/* Exclude sequential-workshop phase pages; each workshop - surfaces via a single overview card below. */}} - {{- $phasePages := where $pages "Params.workshop" "!=" nil -}} - {{- $pages = complement $phasePages $pages -}} - {{/* One card per workshop overview (the workshop's _index.md). */}} - {{- $workshops := where $pctx.Pages "Params.workshop_overview" true - -}} -
+ + {{ $pctx := .Site }} + {{/* Featured set: opt-in `featured: true` singles OR workshop overviews. */}} + {{- $featured := where (where $pctx.RegularPages ".Section" "tutorials") "Params.featured" true -}} + {{- $workshops := where $pctx.Pages "Params.workshop_overview" true -}} + {{- $cards := $workshops | union $featured -}} + + {{ with $cards }} + - {{ else }} -
-
- - - - - -
-
-
-
-
-
-
-
-
-
- -
-
- -
{{ end }} - - + +

+ Browse all tutorials → +

Find more examples of how Viam is being used in the world by @@ -242,28 +107,5 @@

Javascript

{{ partial "scripts.html" . }} - - - - - - {{ $jsTutorials := resources.Get "js/tutorials.js" }} {{ if (eq (substr - .Site.BaseURL -1) "/" ) }} {{- $opts := dict "params" (dict "baseURL" - (substr .Site.BaseURL 0 -1 )) -}} {{- $jsTutorials = $jsTutorials | js.Build - $opts -}} {{ $jsTutorials := $jsTutorials | minify }} - - {{ else }} {{- $opts := dict "params" (dict "baseURL" .Site.BaseURL) -}} {{- - $jsTutorials = $jsTutorials | js.Build $opts -}} {{ $jsTutorials := - $jsTutorials | minify }} - - {{ end }} From a626293c3c9254d088b3f411620d387b99eafc9d Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 10:53:06 -0400 Subject: [PATCH 23/44] refactor(tutorials): card heading semantics, scoped badge classes, tighter workshop scope Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- assets/scss/_styles_project.scss | 9 ++++++--- layouts/docs/tutorials.html | 2 +- layouts/partials/tutorial-card.html | 14 +++++++------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index 32817d20c9..25a9446d07 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -4204,6 +4204,9 @@ footer { text-decoration: none; color: inherit; } +.tutorial-card > a { + padding: 0; +} .tutorial-card__body { padding: 20px 22px; } @@ -4222,7 +4225,7 @@ footer { flex-wrap: wrap; gap: 6px; } -.tutorial-card__badges .badge { +.tutorial-card__badge { display: inline-block; font-size: 0.72rem; font-weight: 600; @@ -4233,11 +4236,11 @@ footer { background: #f0f0f2; color: #282829; } -.tutorial-card__badges .badge--workshop { +.tutorial-card__badge--workshop { background: #282829; color: #fff; } -.tutorial-card__badges .badge--lang { +.tutorial-card__badge--lang { background: #e6effa; color: #1b4f8a; } diff --git a/layouts/docs/tutorials.html b/layouts/docs/tutorials.html index 50d048b9d4..50db7ccc3d 100644 --- a/layouts/docs/tutorials.html +++ b/layouts/docs/tutorials.html @@ -49,7 +49,7 @@

{{ .Title }}

{{ $pctx := .Site }} {{/* Featured set: opt-in `featured: true` singles OR workshop overviews. */}} {{- $featured := where (where $pctx.RegularPages ".Section" "tutorials") "Params.featured" true -}} - {{- $workshops := where $pctx.Pages "Params.workshop_overview" true -}} + {{- $workshops := where (where $pctx.Pages ".Section" "tutorials") "Params.workshop_overview" true -}} {{- $cards := $workshops | union $featured -}} {{ with $cards }} diff --git a/layouts/partials/tutorial-card.html b/layouts/partials/tutorial-card.html index 15237b1b27..4c0fe3df30 100644 --- a/layouts/partials/tutorial-card.html +++ b/layouts/partials/tutorial-card.html @@ -2,16 +2,16 @@ {{- $isWorkshop := $p.Params.workshop_overview -}} {{- $phaseCount := cond $isWorkshop (len $p.RegularPages) 0 -}}
- +
-
{{ $p.LinkTitle }}
+

{{ $p.LinkTitle }}

{{- with $p.Description }}

{{ . }}

{{ end -}}
- {{- if $isWorkshop }}Workshop{{ end -}} - {{- with $p.Params.level }}{{ . }}{{ end -}} - {{- with $p.Params.time_estimate }}{{ . }}{{ end -}} - {{- if gt $phaseCount 0 }}{{ $phaseCount }} phases{{ end -}} - {{- range $p.Params.languages }}{{ . }}{{ end -}} + {{- if $isWorkshop }}Workshop{{ end -}} + {{- with $p.Params.level }}{{ . }}{{ end -}} + {{- with $p.Params.time_estimate }}{{ . }}{{ end -}} + {{- if gt $phaseCount 0 }}{{ $phaseCount }} phases{{ end -}} + {{- range $p.Params.languages }}{{ . }}{{ end -}}
From f5432b3fc5558f3937d0266ac87d66a187b89971 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 10:57:09 -0400 Subject: [PATCH 24/44] feat(tutorials): show workshop phase list in sidebar on workshop pages Create layouts/partials/sidebar.html as a project override that branches on .Params.workshop: workshop pages get a targeted sidebar-workshop.html partial listing all phases with the current one highlighted; all other pages fall through to a verbatim copy of the Docsy default sidebar logic, preserving existing behaviour site-wide. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- layouts/partials/sidebar-workshop.html | 26 ++++++++++++++++++++++++ layouts/partials/sidebar.html | 28 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 layouts/partials/sidebar-workshop.html create mode 100644 layouts/partials/sidebar.html diff --git a/layouts/partials/sidebar-workshop.html b/layouts/partials/sidebar-workshop.html new file mode 100644 index 0000000000..14e1deb1c2 --- /dev/null +++ b/layouts/partials/sidebar-workshop.html @@ -0,0 +1,26 @@ +{{- $cur := . -}} +{{- $section := .CurrentSection -}} +{{- $phases := $section.RegularPages.ByWeight -}} +
+ +
diff --git a/layouts/partials/sidebar.html b/layouts/partials/sidebar.html new file mode 100644 index 0000000000..e8f39af52a --- /dev/null +++ b/layouts/partials/sidebar.html @@ -0,0 +1,28 @@ +{{ if .Params.workshop -}} + {{ partial "sidebar-workshop.html" . }} +{{ else -}} + {{/* Verbatim copy of themes/docsy/layouts/partials/sidebar.html */}} + {{/* The "active" toggle here may delay rendering, so we only cache this side bar menu for bigger sites. +*/}}{{ $sidebarCacheLimit := cond (isset .Site.Params.ui "sidebar_cache_limit") .Site.Params.ui.sidebar_cache_limit 2000 -}} +{{ $shouldCache := ge (len .Site.Pages) $sidebarCacheLimit -}} +{{ $sidebarCacheTypeRoot := cond (isset .Site.Params.ui "sidebar_cache_type_root") .Site.Params.ui.sidebar_cache_type_root false -}} +{{ if $shouldCache -}} + {{ $mid := printf "m-%s" (.RelPermalink | anchorize) }} + + {{ partialCached "sidebar-tree.html" . .FirstSection.RelPermalink }} +{{ else -}} + {{ partial "sidebar-tree.html" . }} +{{- end }} +{{- end }} From 2a0df415739ca7cd3f89508d1d1bedc827b71b7b Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 11:04:54 -0400 Subject: [PATCH 25/44] fix(tutorials): restore verbatim sidebar fallback and drop doubled phase numbers Restore the else-branch jQuery in sidebar.html to match Docsy's default byte-for-byte (active-path walks from #{{ $mid }}, not #{{ $mid }}-li), and remove the redundant {{ add $i 1 }}. prefix in sidebar-workshop.html since phase linkTitles already include their numbers. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- layouts/partials/sidebar-workshop.html | 4 ++-- layouts/partials/sidebar.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/layouts/partials/sidebar-workshop.html b/layouts/partials/sidebar-workshop.html index 14e1deb1c2..6bdcca66da 100644 --- a/layouts/partials/sidebar-workshop.html +++ b/layouts/partials/sidebar-workshop.html @@ -12,10 +12,10 @@ {{ $section.LinkTitle }}
    - {{- range $i, $phase := $phases }} + {{- range $phase := $phases }}
  • - {{ add $i 1 }}. {{ $phase.LinkTitle }} + {{ $phase.LinkTitle }}
  • {{- end }} diff --git a/layouts/partials/sidebar.html b/layouts/partials/sidebar.html index e8f39af52a..f65fafef73 100644 --- a/layouts/partials/sidebar.html +++ b/layouts/partials/sidebar.html @@ -13,7 +13,7 @@ $("#td-section-nav a").removeClass("active"); $("#td-section-nav #{{ $mid }}").addClass("active"); $("#td-section-nav #{{ $mid }}-li span").addClass("td-sidebar-nav-active-item"); - $("#td-section-nav #{{ $mid }}-li").parents("li").addClass("active-path"); + $("#td-section-nav #{{ $mid }}").parents("li").addClass("active-path"); $("#td-section-nav li.active-path").addClass("show"); $("#td-section-nav li.active-path").children("input").prop('checked', true); $("#td-section-nav #{{ $mid }}-li").siblings("li").addClass("show"); From 2e82675ec5579712ecbe2b01cfa0d2de8e778758 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 11:11:36 -0400 Subject: [PATCH 26/44] refactor(tutorials): aria-current on active overview link, pin Docsy version note Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- layouts/partials/sidebar-workshop.html | 2 +- layouts/partials/sidebar.html | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/layouts/partials/sidebar-workshop.html b/layouts/partials/sidebar-workshop.html index 6bdcca66da..c527da963d 100644 --- a/layouts/partials/sidebar-workshop.html +++ b/layouts/partials/sidebar-workshop.html @@ -8,7 +8,7 @@ Tutorials
  • - + {{ $section.LinkTitle }}
      diff --git a/layouts/partials/sidebar.html b/layouts/partials/sidebar.html index f65fafef73..d2b0286d2a 100644 --- a/layouts/partials/sidebar.html +++ b/layouts/partials/sidebar.html @@ -1,7 +1,8 @@ {{ if .Params.workshop -}} {{ partial "sidebar-workshop.html" . }} {{ else -}} - {{/* Verbatim copy of themes/docsy/layouts/partials/sidebar.html */}} + {{/* Verbatim copy of themes/docsy/layouts/partials/sidebar.html (Docsy v0.6.0). + Re-diff against the theme file whenever the Docsy module is bumped. */}} {{/* The "active" toggle here may delay rendering, so we only cache this side bar menu for bigger sites. */}}{{ $sidebarCacheLimit := cond (isset .Site.Params.ui "sidebar_cache_limit") .Site.Params.ui.sidebar_cache_limit 2000 -}} {{ $shouldCache := ge (len .Site.Pages) $sidebarCacheLimit -}} From f35524d94cf0496e7f5a13c9731bab2b3d795ffc Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 11:22:29 -0400 Subject: [PATCH 27/44] fix(tutorials): exclude workshop phase pages from search index (toc_hide) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- docs/tutorials/pick-and-place/01-platform-mental-model.md | 1 + docs/tutorials/pick-and-place/02-configure-resources.md | 1 + docs/tutorials/pick-and-place/03-static-positions.md | 1 + docs/tutorials/pick-and-place/04-local-python-script.md | 1 + docs/tutorials/pick-and-place/05-inline-module.md | 1 + docs/tutorials/pick-and-place/_phase-template.md | 1 + 6 files changed, 6 insertions(+) diff --git a/docs/tutorials/pick-and-place/01-platform-mental-model.md b/docs/tutorials/pick-and-place/01-platform-mental-model.md index 5c717c679d..cf1d482686 100644 --- a/docs/tutorials/pick-and-place/01-platform-mental-model.md +++ b/docs/tutorials/pick-and-place/01-platform-mental-model.md @@ -6,6 +6,7 @@ slug: "platform-mental-model" weight: 10 description: "Understand how the Viam cloud, agent, and server fit together before you configure anything." workshop: "pick-and-place" +toc_hide: true phase: 1 phase_total: 5 time_estimate: "15 minutes" diff --git a/docs/tutorials/pick-and-place/02-configure-resources.md b/docs/tutorials/pick-and-place/02-configure-resources.md index 4ac7c8f7ff..09ba241ca9 100644 --- a/docs/tutorials/pick-and-place/02-configure-resources.md +++ b/docs/tutorials/pick-and-place/02-configure-resources.md @@ -6,6 +6,7 @@ slug: "configure-resources" weight: 20 description: "Tour the pre-configured resources, the color-detection vision pipeline, and the control and scene tabs." workshop: "pick-and-place" +toc_hide: true phase: 2 phase_total: 5 time_estimate: "20 minutes" diff --git a/docs/tutorials/pick-and-place/03-static-positions.md b/docs/tutorials/pick-and-place/03-static-positions.md index b07c9e96d8..23a4b1c0ed 100644 --- a/docs/tutorials/pick-and-place/03-static-positions.md +++ b/docs/tutorials/pick-and-place/03-static-positions.md @@ -6,6 +6,7 @@ slug: "static-positions" weight: 30 description: "Save the arm's key poses and configure WorldState obstacles, proving the hardware and motion planning work before you add perception." workshop: "pick-and-place" +toc_hide: true phase: 3 phase_total: 5 time_estimate: "20 minutes" diff --git a/docs/tutorials/pick-and-place/04-local-python-script.md b/docs/tutorials/pick-and-place/04-local-python-script.md index 90ad1ea306..712cdfb3c8 100644 --- a/docs/tutorials/pick-and-place/04-local-python-script.md +++ b/docs/tutorials/pick-and-place/04-local-python-script.md @@ -6,6 +6,7 @@ slug: "local-python-script" weight: 40 description: "Connect from your laptop, run the static sequence, then add perception to complete the pick-and-sort loop." workshop: "pick-and-place" +toc_hide: true phase: 4 phase_total: 5 time_estimate: "30 minutes" diff --git a/docs/tutorials/pick-and-place/05-inline-module.md b/docs/tutorials/pick-and-place/05-inline-module.md index 0e47974aef..6c3c50113e 100644 --- a/docs/tutorials/pick-and-place/05-inline-module.md +++ b/docs/tutorials/pick-and-place/05-inline-module.md @@ -6,6 +6,7 @@ slug: "inline-module" weight: 50 description: "Optional: package your working script as an inline module that runs on the robot." workshop: "pick-and-place" +toc_hide: true phase: 5 phase_total: 5 time_estimate: "15 minutes" diff --git a/docs/tutorials/pick-and-place/_phase-template.md b/docs/tutorials/pick-and-place/_phase-template.md index ca6cd63cff..66e7fb0ba5 100644 --- a/docs/tutorials/pick-and-place/_phase-template.md +++ b/docs/tutorials/pick-and-place/_phase-template.md @@ -6,6 +6,7 @@ slug: "phase-slug" weight: 0 # 10, 20, 30, ... in 10s for sidebar order description: "One-sentence description of this phase." workshop: "pick-and-place" +toc_hide: true phase: 0 phase_total: 5 time_estimate: "NN minutes" From 4a78ca0074964b12fc24d5c1d6281624eec37af7 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Tue, 30 Jun 2026 13:24:22 -0400 Subject: [PATCH 28/44] refactor(tutorials): drop redundant left sidebar on landing and archive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tutorials landing and the /tutorials/all/ archive used the cached Docsy sidebar skeleton, which (with the section's hide_children) only repeated the page title — redundant with the h1 and breadcrumb, and offering no navigation. Remove the sidebar on both and let content span full width; the archive's filter UI is its navigation, and the curated landing is self-contained. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- layouts/docs/tutorials-all.html | 7 +++---- layouts/docs/tutorials.html | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/layouts/docs/tutorials-all.html b/layouts/docs/tutorials-all.html index 12487b9349..f1a7da4f8a 100644 --- a/layouts/docs/tutorials-all.html +++ b/layouts/docs/tutorials-all.html @@ -23,10 +23,9 @@
      - -
      + {{/* No left sidebar on the archive: the filter UI is the navigation, + and a sidebar here would only repeat the page title. */}} +
      {{ if not .Site.Params.ui.breadcrumb_disable }}{{ partial "breadcrumb.html" . }}{{ end }}
      diff --git a/layouts/docs/tutorials.html b/layouts/docs/tutorials.html index 50db7ccc3d..72ed7840df 100644 --- a/layouts/docs/tutorials.html +++ b/layouts/docs/tutorials.html @@ -23,10 +23,9 @@
      - -
      + {{/* No left sidebar on the tutorials landing: it would only repeat the + page title. The full browsable list lives at /tutorials/all/. */}} +
      Date: Tue, 30 Jun 2026 16:42:58 -0400 Subject: [PATCH 29/44] feat(tutorials): redesign landing with a featured workshop hero Replace the lone floating card with a featured hero that earns the full width: a spec-sheet left column (mono eyebrow, title, description, a level/time/phases/language strip, and a primary CTA) beside a high-contrast near-black phase panel that lists the workshop's phases as a clickable, numbered stepper. Keeps Viam's black/white + Space Mono technical aesthetic, constrains the content column for readability, and adds one reduced-motion- aware page-load reveal. Additional featured tutorials flow into a card grid below the hero; the curated set and archive link are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MZUbnKMDUmYy1iRkxkpjyz --- assets/scss/_styles_project.scss | 260 +++++++++++++++++++++++++++- layouts/docs/tutorials.html | 24 ++- layouts/partials/tutorial-hero.html | 67 +++++++ 3 files changed, 335 insertions(+), 16 deletions(-) create mode 100644 layouts/partials/tutorial-hero.html diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index 25a9446d07..63bd3de838 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -1376,10 +1376,10 @@ td > ol { .table .tab-content > div .code-toolbar { border: none; width: fit-content; - max-width: 100%;; + max-width: 100%; } -.table .tab-content .tab-pane { +.table .tab-content .tab-pane { max-width: none; } @@ -3246,14 +3246,12 @@ nav { min-height: 3.5rem; } - @media (min-width: 992px) { .d-lg-block { display: flex !important; } } - ul > li.nav-fold > span > span.link-and-toggle { display: flex !important; flex-direction: row; @@ -3264,7 +3262,6 @@ ul.ul-0 > li.nav-fold:last-child { } @media (min-width: 768px) { - #landing-page-sidebar { display: none; } @@ -3320,8 +3317,6 @@ ul.ul-0 > li.nav-fold:last-child { padding-left: 0.5rem; } - - .td-sidebar-nav__section .ul-1 ul.ul-2 { padding-left: 0.5rem; border-left: 1px solid #e4e4e6; @@ -4248,3 +4243,254 @@ footer { margin-top: 24px; font-weight: 600; } + +/* ── Tutorials landing: featured hero + spec-sheet aesthetic ───────────── */ +.td-content.tutorials { + max-width: 1080px; +} +.tutorials-intro h1 { + margin-bottom: 0.35rem; +} +.tutorials-intro > p { + color: #4a4a4f; + font-size: 1.05rem; + max-width: 60ch; +} + +.tutorial-hero { + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); + margin: 2rem 0 2.5rem; + border: 1px solid #e4e4e6; + border-radius: 12px; + overflow: hidden; + background: #fff; + box-shadow: + 0 1px 2px rgba(20, 20, 25, 0.04), + 0 18px 40px -22px rgba(20, 20, 25, 0.22); +} +.tutorial-hero--solo { + grid-template-columns: 1fr; +} + +.tutorial-hero__main { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 2.5rem 2.75rem; +} +.tutorial-hero__eyebrow { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-family: "Space Mono", ui-monospace, monospace; + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #8a8a90; +} +.tutorial-hero__eyebrow::before { + content: ""; + width: 20px; + height: 1px; + background: currentColor; +} +.tutorial-hero__title { + margin: 0.85rem 0 0.7rem; + font-size: 2rem; + line-height: 1.08; + font-weight: 800; + letter-spacing: -0.01em; +} +.tutorial-hero__title a { + color: #1c1c1f; + text-decoration: none; +} +.tutorial-hero__title a:hover { + text-decoration: underline; + text-underline-offset: 3px; +} +.tutorial-hero__desc { + margin: 0 0 1.4rem; + max-width: 48ch; + color: #4a4a4f; + font-size: 1.02rem; +} +.tutorial-hero__spec { + display: flex; + flex-wrap: wrap; + margin: 0 0 1.6rem; + border-top: 1px solid #ececee; +} +.tutorial-hero__spec > div { + margin-top: 0.9rem; + padding: 0 1.6rem 0 0; +} +.tutorial-hero__spec dt { + margin: 0 0 0.2rem; + font-family: "Space Mono", ui-monospace, monospace; + font-size: 0.64rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: #9a9aa0; +} +.tutorial-hero__spec dd { + margin: 0; + font-weight: 600; + font-size: 0.92rem; + color: #1c1c1f; + text-transform: capitalize; +} +.tutorial-hero__cta { + margin-top: auto; + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.7rem 1.25rem; + border-radius: 6px; + background: #1c1c1f; + color: #fff; + font-family: "Space Mono", ui-monospace, monospace; + font-size: 0.8rem; + letter-spacing: 0.04em; + text-transform: uppercase; + text-decoration: none; + transition: + background 0.15s ease, + transform 0.15s ease; +} +.tutorial-hero__cta:hover { + background: #000; + color: #fff; + text-decoration: none; +} +.tutorial-hero__cta span { + transition: transform 0.15s ease; +} +.tutorial-hero__cta:hover span { + transform: translateX(3px); +} + +/* Dark phase panel — the one high-contrast, on-brand moment */ +.tutorial-hero__phases { + padding: 2.1rem 2.1rem 1.9rem; + background-color: #161618; + background-image: radial-gradient( + circle at 100% 0%, + rgba(255, 255, 255, 0.06), + transparent 55% + ); + color: #e8e8ea; +} +.tutorial-hero__phases-label { + display: block; + margin-bottom: 1rem; + font-family: "Space Mono", ui-monospace, monospace; + font-size: 0.66rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: #7d7d86; +} +.tutorial-hero__phases ol { + margin: 0; + padding: 0; + list-style: none; +} +.tutorial-hero__phases li { + border-top: 1px solid rgba(255, 255, 255, 0.08); +} +.tutorial-hero__phases li:first-child { + border-top: 0; +} +.tutorial-hero__phases a { + display: flex; + align-items: baseline; + gap: 0.9rem; + padding: 0.7rem 0; + color: #c8c8ce; + text-decoration: none; + transition: + color 0.15s ease, + padding-left 0.15s ease; +} +.tutorial-hero__phases a:hover { + padding-left: 4px; + color: #fff; + text-decoration: none; +} +.tutorial-hero__phases .num { + flex: none; + font-family: "Space Mono", ui-monospace, monospace; + font-size: 0.78rem; + color: #6e6e78; + transition: color 0.15s ease; +} +.tutorial-hero__phases a:hover .num { + color: #fff; +} +.tutorial-hero__phases .label { + font-size: 0.92rem; + line-height: 1.3; +} + +.tutorials-grid { + margin-top: 0.5rem; +} + +.browse-all-tutorials { + margin-top: 1.75rem; +} +.browse-all-link { + font-family: "Space Mono", ui-monospace, monospace; + font-size: 0.8rem; + letter-spacing: 0.04em; + text-transform: uppercase; + text-decoration: none; + color: #1c1c1f; + border-bottom: 2px solid #1c1c1f; + padding-bottom: 2px; +} +.browse-all-link:hover { + text-decoration: none; + color: #1c1c1f; +} +.browse-all-link span { + display: inline-block; + transition: transform 0.15s ease; +} +.browse-all-link:hover span { + transform: translateX(4px); +} + +/* One orchestrated page-load reveal; respects reduced-motion */ +@media (prefers-reduced-motion: no-preference) { + .tutorial-hero { + animation: tutorial-hero-rise 0.5s cubic-bezier(0.2, 0.7, 0.2, 1) both; + } + .tutorial-hero__phases li { + animation: tutorial-hero-rise 0.4s cubic-bezier(0.2, 0.7, 0.2, 1) both; + animation-delay: calc(140ms + var(--i) * 60ms); + } + @keyframes tutorial-hero-rise { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: none; + } + } +} + +@media (max-width: 800px) { + .tutorial-hero { + grid-template-columns: 1fr; + } + .tutorial-hero__main { + padding: 1.9rem 1.6rem; + } + .tutorial-hero__phases { + padding: 1.6rem; + } +} diff --git a/layouts/docs/tutorials.html b/layouts/docs/tutorials.html index 72ed7840df..9e1e51f438 100644 --- a/layouts/docs/tutorials.html +++ b/layouts/docs/tutorials.html @@ -39,30 +39,36 @@ {{ if not .Site.Params.ui.breadcrumb_disable }}{{ partial "breadcrumb.html" . }}{{ end }}
      -

      {{ .Title }}

      -

      - Hands-on guides that walk you through building a working project with Viam, - step by step. -

      +
      +

      {{ .Title }}

      +

      + Hands-on guides that walk you through building a working + project with Viam, step by step. +

      +
      {{ $pctx := .Site }} {{/* Featured set: opt-in `featured: true` singles OR workshop overviews. */}} {{- $featured := where (where $pctx.RegularPages ".Section" "tutorials") "Params.featured" true -}} {{- $workshops := where (where $pctx.Pages ".Section" "tutorials") "Params.workshop_overview" true -}} - {{- $cards := $workshops | union $featured -}} + {{- $cards := sort ($workshops | union $featured) ".Weight" -}} {{ with $cards }} -

      diff --git a/layouts/partials/tutorial-hero.html b/layouts/partials/tutorial-hero.html new file mode 100644 index 0000000000..156247ddc1 --- /dev/null +++ b/layouts/partials/tutorial-hero.html @@ -0,0 +1,67 @@ +{{- /* Featured hero for the flagship tutorial/workshop on the tutorials +landing. For workshops, the right panel lists phases as a clickable numbered +stepper. */ -}} {{- $p := .page -}} {{- $isWorkshop := +$p.Params.workshop_overview -}} {{- $phases := cond $isWorkshop +$p.RegularPages.ByWeight slice -}} {{- $phaseCount := len $phases -}} {{- $start +:= cond (gt $phaseCount 0) (index $phases 0).RelPermalink $p.RelPermalink -}} +

      +
      + {{ if $isWorkshop }}Featured workshop{{ else }}Featured tutorial{{ end + }} +

      + {{ $p.LinkTitle }} +

      + {{- with $p.Description }} +

      {{ . }}

      + {{ end }} +
      + {{- with $p.Params.level }} +
      +
      Level
      +
      {{ . }}
      +
      + {{ end }} {{- with $p.Params.time_estimate }} +
      +
      Time
      +
      {{ . }}
      +
      + {{ end }} {{- if gt $phaseCount 0 }} +
      +
      Phases
      +
      {{ $phaseCount }}
      +
      + {{ end }} {{- with $p.Params.languages }} +
      +
      Language
      +
      {{ delimit . ", " }}
      +
      + {{ end }} +
      + {{ if $isWorkshop }}Start the workshop{{ else }}Start tutorial{{ end }} + +
      + {{- if gt $phaseCount 0 }} + + {{- end }} +
      From 24ff5ef1b0e30677435cc965e9ef9430cfa45282 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 09:49:19 -0400 Subject: [PATCH 30/44] docs(plans): design for self-serve pick-and-place authoring Restructure to 6 phases (split perception into Phase 5, rename Phase 4, renumber module to Phase 6) and author/reconcile every page to the finalized self-serve plan, grounded in the published companion repo. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01B26gLpfchSJJMgUg6rCFKS --- ...k-and-place-self-serve-authoring-design.md | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 plans/2026-07-02-pick-and-place-self-serve-authoring-design.md diff --git a/plans/2026-07-02-pick-and-place-self-serve-authoring-design.md b/plans/2026-07-02-pick-and-place-self-serve-authoring-design.md new file mode 100644 index 0000000000..7be7181974 --- /dev/null +++ b/plans/2026-07-02-pick-and-place-self-serve-authoring-design.md @@ -0,0 +1,274 @@ +# Design: Restructure + Author the Pick-and-Place Workshop to the Finalized Self-Serve Plan + +**Date:** 2026-07-02 +**Status:** Approved (brainstorm complete; ready for implementation planning) +**Author:** nick.hehr (with Claude Code) +**Branch:** `workshop-format-pick-and-place` +**Related:** + +- `../pick-and-place/pick-n-place-tutorial-plan.md` — the finalized content spec (source of truth for page content) +- `../pick-and-place/tutorial-review-notes.md` — per-phase self-serve decision log +- `../pick-and-place/docs/plans/2026-07-01-self-serve-tutorial-review-fixes.md` — the upstream plan that produced the finalized spec (explicitly defers "authoring the Hugo content pages" to this effort) +- `plans/2026-06-30-tutorials-featured-index-and-workshop-nav-design.md` — landing/index/sidebar work already on this branch (unchanged by this effort) + +## Context + +The multi-page pick-and-place workshop lives at `docs/tutorials/pick-and-place/` +(Hugo + Docsy). It currently has **five** phase pages, of which only Phase 3 is +substantially authored; Phases 1, 2, 4, 5 are structured stubs (headings + +`` markers). The pages were written against an **earlier, +facilitated-workshop** version of the plan. + +Meanwhile the plan has been finalized as a **self-serve** tutorial +(`pick-n-place-tutorial-plan.md`), incorporating a full review pass +(`tutorial-review-notes.md`). The upstream review-fixes plan updated the spec +and explicitly deferred authoring the Hugo pages — **that deferred authoring is +this effort.** + +The companion code repo is published at + (local working copy at +`../pick-and-place/`), containing `scripts/starter-script.py`, +`scripts/reference-solution.py`, `scripts/pyproject.toml`, +`config/machine-fragment.json`, `config/obstacles-template.json`, and +`setup/frame-calibration-worksheet.md`. All code and JSON in the authored pages +is grounded in this repo, not invented. + +## The gap (why this is more than "fill the TODOs") + +The finalized plan differs from what is on the branch in three ways: + +1. **Structural (5 → 6 phases).** Perception splits out of Phase 4 into a + dedicated Phase 5; the inline module moves to Phase 6; Phase 4 is renamed + from "Local Python script" to "Control the robot from Python" and covers the + static sequence only. +2. **Voice (facilitated → self-serve).** The already-authored `_index.md` and + Phase 3 actively contradict the finalized plan and must be rewritten, not + just extended. Examples on the branch today: + - `_index.md`: "Hardware pre-provisioned for you… if you are in a guided + workshop" — the plan wants self-serve two-milestone + prerequisites-gate + framing. + - `03-static-positions.md`: "Your workshop facilitator provides the table and + bin dimensions" and "the machine is already configured with + arm-position-saver switches… pre-loaded configuration" — the plan + overturns both (measure your own workspace; configure every resource by + hand; `machine-fragment.json` is a check-your-work reference, not an + import). +3. **Net-new content.** Every TODO section across the phases must be authored to + the plan's per-page spec, grounded in the companion code. + +## Goals + +1. Ship a six-phase, self-serve pick-and-place workshop whose content matches + `pick-n-place-tutorial-plan.md` and whose code/JSON is verified against the + published companion repo. +2. Preserve the branch's established house style and the landing/index/sidebar + work already done — the sidebar/nav shortcodes are load-bearing and must keep + working. +3. Keep the build green at every commit. + +## Non-goals + +- No changes to the landing page, `/tutorials/all/` archive, `tutorial-card`, + `tutorial-hero`, or `sidebar.html` work from the 2026-06-30 design. +- No new shortcodes. Author with the existing + `workshop-phases` / `workshop-nav` / `checkpoint` / `alert` shortcodes. The + plan's speculative `code-file` shortcode and `data/tutorials.yaml` stay + deferred; code is shown as inline fenced blocks with links to the companion + repo. +- No companion-repo changes and no separate hardware-setup guide in this effort + (the plan references both; they are downstream). The `_index` links to the + setup guide as "forthcoming." +- No edits to the facilitated workshop slides. + +## House style (authoring must follow the branch, not the plan's speculative schema) + +The finalized plan sketches a speculative frontmatter/shortcode schema +(`tutorial:`, `difficulty:`, `checkpoint.html`, `content/`). **Ignore it** in +favor of the conventions the branch actually uses: + +- **Directory:** `docs/tutorials/pick-and-place/` (this repo uses `docs/`, not + `content/`). +- **Frontmatter keys:** `title`, `linkTitle` (numbered, e.g. `"4. Control from + Python"`), `type: "docs"`, `slug`, `weight`, `description`, `workshop: + "pick-and-place"`, `toc_hide: true`, `phase`, `phase_total`, `time_estimate`, + `prev`, `next`, `languages`. +- **Shortcodes:** `{{< workshop-phases >}}` (top of body), `{{< checkpoint >}}`, + `{{< alert title="…" color="note" >}}`, `{{< workshop-nav >}}` (bottom). +- **Companion links:** real `https://github.com/viam-devrel/pick-and-place` + URLs. +- **Prose rules:** enforced by prettier, markdownlint (`.markdownlint.yaml`), + and vale (no em dashes, sentence-case headings, no parenthetical plurals, + etc.). + +## Design + +### Part 1 — Restructure (single prep commit; mechanical; build stays green) + +| Action | File | Key frontmatter | +| --- | --- | --- | +| Keep | `01-platform-mental-model.md` | `phase: 1`, `phase_total: 6`, `weight: 10`, slug `platform-mental-model` | +| Keep | `02-configure-resources.md` | `phase: 2`, `phase_total: 6`, `weight: 20`, slug `configure-resources` | +| Keep | `03-static-positions.md` | `phase: 3`, `phase_total: 6`, `weight: 30`, slug `static-positions` | +| Rename + strip perception | `04-local-python-script.md` → `04-control-the-robot-from-python.md` | title "Phase 4: Control the robot from Python", slug `control-the-robot-from-python`, `phase: 4`, `phase_total: 6`, `weight: 40` | +| New file | `05-perception-guided-picking.md` | title "Phase 5: Perception-guided picking", slug `perception-guided-picking`, `phase: 5`, `phase_total: 6`, `weight: 50` | +| Renumber | `05-inline-module.md` → `06-inline-module.md` | title "Phase 6: Inline module", slug `inline-module`, `phase: 6`, `phase_total: 6`, `weight: 60` | + +Also in this commit: + +- Bump `phase_total: 6` on every phase page. +- Rewire the `prev`/`next` chain to: + `platform-mental-model` → `configure-resources` → `static-positions` → + `control-the-robot-from-python` → `perception-guided-picking` → + `inline-module`. +- Rebuild the `_index.md` phase list to six entries with the plan's time + estimates (15 / 20 / 20 / 15 / 22 / 20 min) and corrected links/titles. +- Move the perception-related TODO scaffolding out of Phase 4 and into the new + Phase 5 stub so Phase 4 covers the static sequence only. + +The `workshop-phases` / `workshop-nav` shortcodes derive phase order from +`section.RegularPages.ByWeight`, so the new Phase 5 page auto-slots once its +`weight` is set. No aliases/redirects are required: the branch is not merged or +live, so no public URLs change. The Phase 4 slug change +(`local-python-script` → `control-the-robot-from-python`) is therefore safe. + +### Part 2 — Per-page content (one authoring commit each, grounded in companion code) + +Content requirements are the per-page spec in `pick-n-place-tutorial-plan.md`; +the self-serve rationale is in `tutorial-review-notes.md`. Summary per page: + +- **`_index.md` (rewrite, facilitated → self-serve).** Two-milestone framing + (Phase 4 = milestone one, "drive the robot from your own code," a bankable + win; Phase 5 = milestone two, perception; Phase 6 optional; **Phases 1–5 + core**). Prerequisites **gate** with verify commands *and* install links + (Python 3.10+, `viam-sdk`, a working terminal, a Viam account with a Live + machine). Login/machine-access as a prerequisite. Environment validation + (`uv` recommended, can `import viam`) before Phase 4. Hardware context via the + setup-guide link + header image, not a tour. Two entry paths distinguishing + hardware provisioning (may be pre-provisioned) from resource configuration + (always hands-on). Link to companion repo. Remove resolved TODOs. +- **P1 `01-platform-mental-model.md`.** Three-layer architecture, SDK + connection, config-as-source-of-truth, components vs. services, dependency + graph. Live "open your CONFIGURE tab, find `arm-1`, read its + `namespace:family:model`" grounding throughout (overrides the old "no live + interactions yet"). Keep `shape-detector` / `vision-segment` foreshadow in the + dependency graph. Builtin (RDK) vs. module-provided resources, with the + module-download moment previewed (it lands in P2). Closing self-check. +- **P2 `02-configure-resources.md`.** Hands-on config of `arm-1`, `gripper-1`, + `cam-1` (resource table as target state, not "what's pre-configured"). + Configuring `viam:ufactory:xArm6` is the module-download moment. 3D-scene + active task: "jog joint 1, watch `cam-1` move with the arm" (sets up the + wrist-camera rule). Gripper `IsHoldingSomething` active task. Per-card + checkpoints (camera, arm, gripper). Vision pipeline is **not** configured here + — it moves to P5. +- **P3 `03-static-positions.md` (self-serve rewrite).** Problem-isolation + rationale kept as proof of value (real production workflow). The five key + poses. Explicit hands-on arm-position-saver setup (`erh:vmodutils` from + Registry, one switch per pose, `arm: arm-1`), using the app's "duplicate" + feature for poses 2–5; remove "already configured / pre-loaded" framing. + Teach frame-geometry config and have the learner **measure their own + workspace** (remove "facilitator provides dimensions"). Safety walls framed + as a production-motion feature. SetPosition `1`=save / `2`=execute callout + + "does nothing" troubleshooting aside. `machine-fragment.json` / + `obstacles-template.json` as check-your-work references. Reconcile the + obstacle JSON against the real `config/obstacles-template.json` and the Viam + motion-service schema (resolves the existing "illustrative JSON" TODO). +- **P4 `04-control-the-robot-from-python.md`.** Why script before module + (comparison). Sell programmability + the Control-tab-UI→SDK-method-name + mapping. Clone the companion `scripts/` project and `uv run starter-script.py` + (`uv` primary, pip fallback; env already validated in the prerequisites gate). + Reference the Connect-tab boilerplate rather than authoring the connection + from scratch. Secrets-handling note. Obstacles live in machine config and + apply to every `motion.move` automatically — **not** passed in code. + Checkpoints: `resource_names` prints all resources; static sequence runs + end-to-end from Python. **Static sequence only — no perception.** +- **P5 `05-perception-guided-picking.md` (new).** Configure the vision pipeline + here (shape-detector → `vision-segment`, `detections-to-segments`) + + Control-tab test. Frame system + `transform_pose`. **Home-pose guard clause** + in the perception loop (assert/return to `home-pose` before every detect; + structurally enforced; first entry in the debugging guide). Worked + approach-offset as a fully worked example; learner practices the + gripper-TCP grasp offset (productive struggle). Explicit + `motion.move("gripper-1", …)` frame semantics (drives the gripper frame to the + world pose — contrast with the arm/UI MoveToPosition). Perception API using + `len(o.point_cloud)`. Symptom → 3D-scene-tab debugging with a back-link to P3 + obstacle/safety-wall config. Granular sub-checkpoints (detector works → + transform yields sane world coords → approach reachable → grasp succeeds). All + code verified against `reference-solution.py`, including the gripper-TCP + offset value and `PoseInFrame` kwargs. +- **P6 `06-inline-module.md` (optional).** Framed as an optional next step (no + time pressure). Strong "why bother" (survives disconnection, auto-restart, OTA + deploy, scheduled runs). Honest "mostly packaging + one real change." Tiered + scope: MVP (repackage + `do_command`, manual trigger) vs. level-2 (scheduled + job / autonomous). `validate_config` + `reconfigure` (dependency injection). + **Corrected `transform_pose`-in-module pattern:** a single reused in-module + `RobotClient` authenticated from `VIAM_API_KEY` / `VIAM_API_KEY_ID` / + `VIAM_MACHINE_FQDN` env vars — there is **no** `FrameSystemClient` and no + injected frame-system dependency (this reverses the current stub's incorrect + note). `from_robot` ↔ `cast + get_resource_name` bridge callout. ~1 min cloud + build time stated upfront. + +### Part 3 — Correctness grounding + +- Python snippets are drawn from / verified against + `../pick-and-place/scripts/{starter-script.py,reference-solution.py}`. +- Machine-config and obstacle JSON are verified against + `../pick-and-place/config/{machine-fragment.json,obstacles-template.json}` and + the Viam motion-service obstacle schema. +- Two known correctness items to resolve during authoring: the "illustrative" + obstacle JSON in P3, and the fabricated `FrameSystemClient` note in P6. +- Remove now-stale TODO comments that say the companion repo "does not exist yet" + (it is published). + +### Part 4 — Verification + +Per commit, run the CLAUDE.md pre-PR checks on the changed Markdown, in order: + +1. `npx prettier --write docs/tutorials/pick-and-place/**/*.md` +2. `npx markdownlint-cli --config .markdownlint.yaml docs/tutorials/pick-and-place/**/*.md` +3. `vale sync && vale docs/tutorials/pick-and-place/` +4. `make build-prod` (must complete without errors) + +Spot-check with `hugo server`: the six phases appear in order in the +`workshop-phases` box and the sidebar with the current phase highlighted; +`workshop-nav` prev/next chains correctly across all six; the landing card and +`/tutorials/all/` archive are unaffected. No unit tests (docs site). + +### Part 5 — Sequencing + +Commit this design doc first, then: + +1. Restructure prep commit (Part 1). +2. `_index.md` rewrite. +3. Phase 1 authoring. +4. Phase 2 authoring. +5. Phase 3 self-serve rewrite. +6. Phase 4 authoring. +7. Phase 5 authoring (new page). +8. Phase 6 authoring (corrected module API). + +Each step keeps the build green and passes the four checks before committing. + +## Risks / watch-items + +- **Nav derivation:** confirm the new Phase 5 page slots correctly in + `workshop-phases` / `workshop-nav` (weight-ordered) and that `phase_total: 6` + renders consistently. +- **Slug change:** the Phase 4 slug change must be reflected in every `prev`/`next` + reference and the `_index` phase list; grep for the old + `/local-python-script/` and `/inline-module/` (position 5) URLs after the + restructure. +- **Obstacle JSON schema:** the existing P3 JSON is flagged illustrative; + reconcile against the real template + Viam schema before publishing. +- **Module API correctness:** ensure the P6 rewrite fully removes the + `FrameSystemClient` pattern and matches the in-module `RobotClient` reference. +- **Scope creep into landing/sidebar:** this effort touches only + `docs/tutorials/pick-and-place/*`; the 2026-06-30 layout work is out of scope. + +## Out-of-scope follow-ups (noted, not now) + +- Companion-repo code changes (e.g. adding the home-pose guard clause to + `reference-solution.py`). +- The separate hardware-setup how-to guide + (`docs/guides/hardware-setup/xarm6-pick-and-place.md`). +- The `code-file` shortcode and `data/tutorials.yaml` card-grid data file. +- Header/hardware-overview imagery for `_index` and the landing card. From 3dea835ba50abebeedcbebe01b51f2b6fe795d72 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 09:53:08 -0400 Subject: [PATCH 31/44] docs(plans): implementation plan for self-serve pick-and-place authoring Nine tasks: restructure to six phases, then author/reconcile _index and Phases 1-6 to the finalized spec, code grounded in the published companion repo (shape detection, single place-pose, component obstacles, corrected in-module RobotClient). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01B26gLpfchSJJMgUg6rCFKS --- ...-02-pick-and-place-self-serve-authoring.md | 490 ++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 plans/2026-07-02-pick-and-place-self-serve-authoring.md diff --git a/plans/2026-07-02-pick-and-place-self-serve-authoring.md b/plans/2026-07-02-pick-and-place-self-serve-authoring.md new file mode 100644 index 0000000000..a11e0b4804 --- /dev/null +++ b/plans/2026-07-02-pick-and-place-self-serve-authoring.md @@ -0,0 +1,490 @@ +# Pick-and-Place Self-Serve Workshop Authoring — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Restructure the pick-and-place workshop from 5 to 6 phases and author/reconcile every page to the finalized self-serve spec, with all code and JSON verified against the published companion repo. + +**Architecture:** One mechanical restructure commit establishes the 6-phase skeleton (renames, split, renumber, nav chains) with the build green. Then one authoring commit per page (`_index`, Phases 1–6), each grounded in the finalized content spec and the real companion code, each passing the four pre-PR checks. A final consistency sweep closes it out. + +**Tech Stack:** Hugo + Docsy static site (Markdown under `docs/tutorials/pick-and-place/`), project shortcodes (`workshop-phases`, `workshop-nav`, `checkpoint`, `alert`), prettier / markdownlint / vale / `make build-prod`. + +**Sources of truth:** + +- Content spec (per-page requirements): `../pick-and-place/pick-n-place-tutorial-plan.md` +- Self-serve rationale / decisions: `../pick-and-place/tutorial-review-notes.md` +- Verified code: `../pick-and-place/scripts/{starter-script.py,reference-solution.py,pyproject.toml,.python-version}` +- Verified config: `../pick-and-place/config/{machine-fragment.json,obstacles-template.json}` +- Design: `plans/2026-07-02-pick-and-place-self-serve-authoring-design.md` + +**Key facts locked from the companion code (do not drift from these):** + +- Detection is **shape-based**: vision service `vision-segment` (model `detections-to-segments`), fed by a `shape-detector`. Objects come from `vision.get_object_point_clouds("cam-1")`; the label is `obj.geometries.geometries[0].label`. +- There are **five poses**, one bin: `home-pose`, `approach-pose`, `grasp-pose`, `travel-pose`, `place-pose`. **Not** per-color bins. +- Poses are saved with `erh:vmodutils` arm-position-saver **switch** components; `set_position(2)` executes a saved pose, `set_position(1)` saves, `set_position(0)` clears. +- Obstacles are **`erh:vmodutils:obstacle` components** (`api: rdk:component:gripper`) added to the machine `components` array — **not** a motion-service WorldState array. They appear in `resource_names` as grippers. +- Motion service name is `"builtin"`. `motion.move(component_name="gripper-1", destination=PoseInFrame(reference_frame="world", pose=...))` drives the **gripper** frame to a world pose. +- Constants: `GRIPPER_LENGTH_MM = 60` (grasp offset), `APPROACH_MM = 100`, settle `0.3 s`. Optional straight-down descent uses `Constraints(linear_constraint=[LinearConstraint(line_tolerance_mm=5.0)])`. +- Python: `requires-python = ">=3.10"`, repo pins `3.11` via `.python-version`. `uv` is primary; pip is fallback. +- Companion repo is **published** at ; remove any "does not exist yet" TODO. + +**House style (follow the branch, not the plan's speculative schema):** frontmatter keys `title`, `linkTitle`, `type: "docs"`, `slug`, `weight`, `description`, `workshop: "pick-and-place"`, `toc_hide: true`, `phase`, `phase_total`, `time_estimate`, `prev`, `next`, `languages`. Body opens with `{{< workshop-phases >}}` and ends with `{{< workshop-nav >}}`. Callouts use `{{< checkpoint >}}` and `{{< alert title="…" color="note" >}}`. Companion links use real `https://github.com/viam-devrel/pick-and-place` URLs. + +**Per-task verification convention:** after editing, run the four checks scoped to the workshop directory, in order (prettier → markdownlint → vale → build). Commit only when all pass. Full commands are in Task 9; abbreviated per task as "run the four checks." + +--- + +### Task 1: Restructure to six phases (mechanical prep commit) + +**Goal:** Establish the 6-phase file/frontmatter/nav skeleton with the build green. No prose authoring yet beyond moving existing stub content. + +**Files:** + +- Rename: `docs/tutorials/pick-and-place/04-local-python-script.md` → `04-control-the-robot-from-python.md` +- Create: `docs/tutorials/pick-and-place/05-perception-guided-picking.md` +- Rename: `docs/tutorials/pick-and-place/05-inline-module.md` → `06-inline-module.md` +- Modify: `01-platform-mental-model.md`, `02-configure-resources.md`, `03-static-positions.md`, `_index.md` + +**Step 1: Rename files with git mv (preserve history)** + +```bash +cd /Users/nick.hehr/src/viam-docs/docs/tutorials/pick-and-place +git mv 04-local-python-script.md 04-control-the-robot-from-python.md +git mv 05-inline-module.md 06-inline-module.md +``` + +**Step 2: Set `phase_total: 6` on all six phase pages** + +In each of `01`…`06`, change `phase_total: 5` to `phase_total: 6`. + +**Step 3: Fix Phase 4 frontmatter (renamed file)** + +In `04-control-the-robot-from-python.md`, set: + +```yaml +title: "Phase 4: Control the robot from Python" +linkTitle: "4. Control from Python" +slug: "control-the-robot-from-python" +weight: 40 +phase: 4 +phase_total: 6 +time_estimate: "15 minutes" +prev: "/tutorials/pick-and-place/static-positions/" +next: "/tutorials/pick-and-place/perception-guided-picking/" +description: "Connect from your laptop and drive the saved static pick-and-place sequence from a Python script." +``` + +Then **remove the perception sections** from the Phase 4 body (the "The frame system and transforms", "Add perception", and "Pass obstacles to the motion service" TODO blocks) — they move to Phase 5 in Step 5. Leave the static-sequence and connection scaffolding. + +**Step 4: Fix Phase 6 frontmatter (renamed file)** + +In `06-inline-module.md`, set: + +```yaml +title: "Phase 6: Inline module" +linkTitle: "6. Inline module" +weight: 60 +phase: 6 +phase_total: 6 +time_estimate: "20 minutes" +prev: "/tutorials/pick-and-place/perception-guided-picking/" +``` + +(Keep `slug: "inline-module"`.) + +**Step 5: Create the Phase 5 stub** + +Create `05-perception-guided-picking.md` with frontmatter and a section skeleton (prose authored in Task 7). Move the perception TODOs removed from Phase 4 here. + +```yaml +--- +title: "Phase 5: Perception-guided picking" +linkTitle: "5. Perception-guided picking" +type: "docs" +slug: "perception-guided-picking" +weight: 50 +description: "Add the vision pipeline and write the perception loop that detects a block, transforms it to the world frame, and picks it with motion planning." +workshop: "pick-and-place" +toc_hide: true +phase: 5 +phase_total: 6 +time_estimate: "22 minutes" +prev: "/tutorials/pick-and-place/control-the-robot-from-python/" +next: "/tutorials/pick-and-place/inline-module/" +languages: ["python"] +--- +``` + +Body: `{{< workshop-phases >}}` at top, `{{< workshop-nav >}}` at bottom, with `## Configure the vision pipeline`, `## The frame system and transform_pose`, `## Detect from home (the wrist-camera rule)`, `## Compute the approach and grasp poses`, `## Run the full pick loop`, `## Debugging guide` section stubs. + +**Step 6: Update the remaining prev/next links for the renumber** + +In `03-static-positions.md`, set `next: "/tutorials/pick-and-place/control-the-robot-from-python/"`. +Grep to confirm no stale slugs remain (old `/local-python-script/`; and `/inline-module/` must now only appear as Phase 5's `next` and Phase 6's own slug): + +```bash +cd /Users/nick.hehr/src/viam-docs +grep -rn "local-python-script" docs/tutorials/pick-and-place/ # expect: zero +grep -rn "phase_total: 5" docs/tutorials/pick-and-place/ # expect: zero +``` + +**Step 7: Update the `_index.md` phase list to six entries (interim)** + +In `_index.md`, replace the five-item Phases list (lines ~41-47) and the "structured as five sequential phases" sentence with six entries and corrected links/titles/estimates. (The full self-serve rewrite of surrounding prose is Task 2; here just keep links valid so the build passes.) + +```markdown +## Phases + +1. **[Platform mental model](/tutorials/pick-and-place/platform-mental-model/)** (~15 min) +2. **[Configure resources and explore the app](/tutorials/pick-and-place/configure-resources/)** (~20 min) +3. **[Static positions and safety obstacles](/tutorials/pick-and-place/static-positions/)** (~20 min) +4. **[Control the robot from Python](/tutorials/pick-and-place/control-the-robot-from-python/)** (~15 min) — milestone one +5. **[Perception-guided picking](/tutorials/pick-and-place/perception-guided-picking/)** (~22 min) — milestone two +6. **[Inline module](/tutorials/pick-and-place/inline-module/)** (~20 min, optional) +``` + +**Step 8: Run the four checks** + +Run prettier → markdownlint → vale → `make build-prod` (see Task 9 for exact commands). Expected: build completes without errors; `workshop-phases`/`workshop-nav` render six phases. + +**Step 9: Commit** + +```bash +git add docs/tutorials/pick-and-place/ +git commit -m "refactor(tutorials): restructure pick-and-place to six phases + +Split perception into Phase 5, rename Phase 4 to control-the-robot-from-python, +renumber inline module to Phase 6, bump phase_total, rewire prev/next." +``` + +--- + +### Task 2: Rewrite `_index.md` (facilitated → self-serve) + +**Goal:** Carry the orientation a facilitator would deliver live: two-milestone framing, a prerequisites gate, self-serve entry paths. + +**Files:** Modify `docs/tutorials/pick-and-place/_index.md` + +**Spec:** `pick-n-place-tutorial-plan.md` → "`pick-and-place/_index.md`" bullets, and `tutorial-review-notes.md` → Phase 0. + +**Content requirements:** + +- **Intro:** six phases; "Phases 1–5 are the core workshop; Phase 6 is optional." Correct the detection narrative to **shape-based sorting into a bin** (not "sorted by color"). Two-milestone framing: Phase 4 (drive the robot from your own code) = milestone one, a bankable win; Phase 5 (perception) = milestone two. +- **What you'll build:** one paragraph — xArm6 + finger gripper + wrist-mounted RealSense; a shape-detection vision service finds blocks, the motion service plans collision-free picks, blocks are placed in the bin; by end of Phase 5 a Python script runs the full detect-pick-place loop. +- **Hardware:** keep the existing four-item list. +- **Phases:** the six-item list from Task 1 Step 7, with the milestone annotations. +- **Prerequisites gate** (replace the current "Hardware pre-provisioned for you / guided workshop" block): + - A checklist with verification commands **and** install links: Python 3.10+ (link to python.org / uv install), the Viam Python SDK (`uv add viam-sdk`; link to SDK docs), a working terminal, and a Viam account with an accessible machine (link to app.viam.com). + - **Login/machine-access as a prerequisite:** "log in at app.viam.com, open your machine, confirm the green **Live** indicator." + - **Environment validation** before Phase 4: a working Python env (`uv` recommended) that can `import viam`: + + ```sh + python3 --version # 3.10 or newer + uv run python -c "import viam; print(viam.__version__)" # prints a version + ``` + + - **Two entry paths**, distinguishing hardware provisioning from resource configuration: "Physical hardware ready → start at Phase 1" / "Provisioning your own hardware → complete the setup guide first (forthcoming)." Note that only physical hardware + viam-agent/server may be pre-provisioned; **resource configuration is always the learner's hands-on work.** +- **Companion code:** keep the `viam-devrel/pick-and-place` link; describe `config/` (check-your-work reference), `scripts/` (starter + reference). **Remove** the "companion repo does not exist yet" TODO. + +**Verification:** run the four checks. `grep -n "pre-provisioned by instructor\|sorted by color\|does not exist yet" docs/tutorials/pick-and-place/_index.md` → expect zero. + +**Commit:** `docs(tutorials): rewrite pick-and-place overview for self-serve (milestones, prerequisites gate)` + +--- + +### Task 3: Author Phase 1 — `01-platform-mental-model.md` + +**Goal:** Concept phase grounded in live app interaction, ending with a self-check. + +**Files:** Modify `docs/tutorials/pick-and-place/01-platform-mental-model.md` + +**Spec:** `pick-n-place-tutorial-plan.md` → "`01-platform-mental-model.md`"; `tutorial-review-notes.md` → Phase 1. + +**Content requirements (replace TODO stubs with prose):** + +- Three questions up top the learner should be able to answer by the end (state them; used by the closing self-check). +- Three-layer architecture (cloud app / `viam-agent` / `viam-server`), SDK connection, config-as-source-of-truth, resource model (components vs. services), the dependency graph. +- **Live grounding** in each section: "open your **CONFIGURE** tab, find `arm-1`, read its `namespace:family:model`," etc. — overrides any "no live interactions yet" stance. +- Keep the perception-pipeline **foreshadow**: use `shape-detector` and `vision-segment` as concrete examples of services / composing resources (they build these in Phase 5). +- **Builtin (RDK) vs. module-provided resources:** most added functionality comes from modules; explain how modules interact with `viam-server`, and preview the module-download moment (it lands in Phase 2 when they add the xArm). +- **Closing self-check** ({{< alert … >}} or plain): "you should now be able to answer the three questions from the top — if not, re-skim." + +**Verification:** run the four checks. + +**Commit:** `docs(tutorials): author Phase 1 platform mental model (self-serve)` + +--- + +### Task 4: Author Phase 2 — `02-configure-resources.md` + +**Goal:** First hands-on phase: configure every resource by hand and verify with test cards. + +**Files:** Modify `docs/tutorials/pick-and-place/02-configure-resources.md` + +**Spec:** `pick-n-place-tutorial-plan.md` → "`02-configure-resources.md`"; `tutorial-review-notes.md` → Phase 2 and the cross-cutting "resources are hands-on" correction. + +**Content requirements:** + +- Learner configures **each** hardware resource by hand: `arm-1` (`viam:ufactory:xArm6`), `gripper-1`, `cam-1`. Present the resource table as **target state**, not "what's pre-configured." +- Configuring the xArm is the **module-download moment**: add the arm, watch `viam-server` download + start the module live (delivers the Phase 1 builtin-vs-module lesson). +- **CONTROL tab test cards** with per-card **checkpoints**: camera card (see a frame), arm card (jog joints), gripper card. +- **3D scene tab active task:** "jog joint 1 and watch the `cam-1` frame move with the arm" — this is the wrist-mounted-camera insight, load-bearing for Phase 5's detect-from-home rule. +- **Gripper `IsHoldingSomething` task:** place a block between the fingers, press **Grab**, observe the status; add a gripper checkpoint for symmetry. +- **The vision pipeline is NOT configured here** — it moves to Phase 5. Remove any vision-service config from this page if present. + +**Verification:** run the four checks. `grep -n "pre-configured\|vision-segment\|shape-detector" docs/tutorials/pick-and-place/02-configure-resources.md` → expect zero (vision belongs to Phase 5). + +**Commit:** `docs(tutorials): author Phase 2 hands-on resource configuration` + +--- + +### Task 5: Rewrite Phase 3 — `03-static-positions.md` (self-serve + model correction) + +**Goal:** Self-serve rewrite AND correct the detection/obstacle model: five poses with a single `place-pose`, obstacles as `erh:vmodutils:obstacle` components, measure-your-own-workspace. + +**Files:** Modify `docs/tutorials/pick-and-place/03-static-positions.md` + +**Spec:** `pick-n-place-tutorial-plan.md` → "`03-static-positions.md`"; `tutorial-review-notes.md` → Phase 3. Verify JSON against `../pick-and-place/config/obstacles-template.json` and pose names against `reference-solution.py`. + +**Content corrections from the current page (all required):** + +1. **Five poses, single bin.** Replace the per-color bin poses (`red-bin-pose`, `blue-bin-pose`, `green-bin-pose`) with the five canonical poses: `home-pose`, `approach-pose`, `grasp-pose`, `travel-pose`, `place-pose`. Update the two tables accordingly. +2. **Hands-on pose setup** (remove "the machine is already configured… pre-loaded configuration"): add `erh:vmodutils` arm-position-saver from the Registry, add one **switch** per pose with `arm: arm-1`; configure `home-pose` fully, then use the app's **"duplicate" resource feature** for the other four. `machine-fragment.json` is the **check-your-work reference**, not an import. +3. **Measure your own workspace** (remove "Your workshop facilitator provides the table and bin dimensions"): teach how frame geometries are configured; the learner measures the table and obstacles with `GetEndPosition` and translates measurements into geometry config. +4. **Obstacles are components, not a WorldState array.** Replace the current `{"obstacles": [...]}` JSON with the real `erh:vmodutils:obstacle` component form. Use this verbatim (from `obstacles-template.json`), noting `REPLACE_WITH_MEASURED_*` placeholders and that these appear in `resource_names` as grippers: + + ```json + { + "name": "table", + "api": "rdk:component:gripper", + "model": "erh:vmodutils:obstacle", + "attributes": { + "geometries": [ + { + "label": "table", + "type": "box", + "x": 1200, + "y": 800, + "z": 30, + "translation": { "x": 0, "y": 0, "z": -15 }, + "parent": "world" + } + ] + } + } + ``` + + Explain: box `z` translation is the box **center** (half its height); the table sits below `world` z=0 so its z is negative half-thickness (`-15` for a 30 mm top). Then the two **safety walls** (`safety-wall-front`, `safety-wall-side`) as thin vertical boxes at the workspace boundary with `REPLACE_WITH_MEASURED_*` positions. +5. **Safety walls as a production-motion feature** — configured to fit the learner's workspace; pitch virtual walls as a demo of the motion planner honoring obstacles for real-world/production deployments, not just classroom safety. +6. **Keep the problem-isolation rationale** as proof of value (pose-to-pose motion is a real production workcell workflow). +7. **Keep** the SetPosition `1`=save / `2`=execute callout, plus the "SetPosition(2) does nothing → you didn't save first" troubleshooting aside. +8. Link `obstacles-template.json` and `machine-fragment.json` in the companion repo as check-your-work references; remove the "illustrative JSON must be reconciled" TODO once the JSON matches the template. + +**Verification:** run the four checks. `grep -n "facilitator provides\|red-bin\|blue-bin\|green-bin\|\"obstacles\"" docs/tutorials/pick-and-place/03-static-positions.md` → expect zero. `grep -n "erh:vmodutils:obstacle\|place-pose" …/03-static-positions.md` → expect hits. + +**Commit:** `docs(tutorials): rewrite Phase 3 for self-serve (five poses, component obstacles, measured workspace)` + +--- + +### Task 6: Author Phase 4 — `04-control-the-robot-from-python.md` + +**Goal:** Drive the **static** sequence from Python. No perception. + +**Files:** Modify `docs/tutorials/pick-and-place/04-control-the-robot-from-python.md` + +**Spec:** `pick-n-place-tutorial-plan.md` → "`04-control-the-robot-from-python.md`"; `tutorial-review-notes.md` → Phase 4. Code from `../pick-and-place/scripts/starter-script.py`. + +**Content requirements:** + +- **Why a script before a module** (comparison): programmability (loops/branches/logic) AND the Control-tab-UI-controls → SDK-method-name mapping (the cards map to methods). Make the payoff felt. +- **Get the companion project:** clone/download `viam-devrel/pick-and-place`, work in `scripts/`. `uv` is primary (`uv run python starter-script.py`; it reads `pyproject.toml`/`.python-version`); pip is fallback. Env was already validated in the prerequisites gate — this phase is connect + run. +- **Connection:** reference the **Connect tab → Python SDK** boilerplate; the starter's `connect()` mirrors it. Show the connection block and the `MACHINE_ADDRESS`/`API_KEY`/`API_KEY_ID` fill-ins. **Secrets note:** don't commit API keys; use the repo `.gitignore` or env vars. +- **Verify the connection** with `print(machine.resource_names)` — a **checkpoint**: you should see `arm-1`, `gripper-1`, `cam-1`, the poses as switches, and the obstacles as grippers. +- **Run the static sequence** (verbatim, matches `starter-script.py` TODO 4): + + ```python + await home.set_position(2) + await approach.set_position(2) + await gripper.open() + await grasp.set_position(2) + await gripper.grab() + await asyncio.sleep(0.3) # finger gripper settle + await travel.set_position(2) + await place_pose.set_position(2) + await gripper.open() + await home.set_position(2) + ``` + +- **Obstacles are not passed in code** — they live in the machine config (Phase 3) and apply to every `motion.move` automatically. No runtime WorldState. +- **Checkpoints:** `resource_names` prints all resources; the static sequence runs end-to-end from Python. +- Connection-debugging aside for common failures. + +**Verification:** run the four checks. `grep -n "get_object_point_clouds\|transform_pose\|vision" …/04-control-the-robot-from-python.md` → expect zero (perception is Phase 5). + +**Commit:** `docs(tutorials): author Phase 4 driving the static sequence from Python` + +--- + +### Task 7: Author Phase 5 — `05-perception-guided-picking.md` (new page) + +**Goal:** The hardest phase: configure vision, transform to world, compute offsets, run the pick loop. Code verified against `reference-solution.py`. + +**Files:** Modify `docs/tutorials/pick-and-place/05-perception-guided-picking.md` + +**Spec:** `pick-n-place-tutorial-plan.md` → "`05-perception-guided-picking.md`"; `tutorial-review-notes.md` → Phase 5. + +**Content requirements:** + +- **Configure the vision pipeline here** (moved from Phase 2): a `shape-detector` feeding `vision-segment` (model `detections-to-segments`); test it in the **CONTROL** tab. First sub-checkpoint: detector works in the app. +- **Frame system + `transform_pose`:** the detection is in the `cam-1` frame; the planner needs `world`. Show: + + ```python + obj_in_cam = PoseInFrame(reference_frame=CAMERA_NAME, pose=geometry.center) + obj_in_world = await machine.transform_pose(obj_in_cam, "world") + ``` + +- **Detect from home (wrist-camera rule):** the camera is wrist-mounted, so its frame moves with the arm — you MUST detect from `home-pose`. **Home-pose guard clause** structurally enforced (`await home.set_position(2)` before every detect) and made the **first** entry in the debugging guide. +- **Detection** (verbatim from reference): + + ```python + objects = await vision.get_object_point_clouds(CAMERA_NAME) + if not objects: + print("No objects detected") + return False + obj = max(objects, key=lambda o: len(o.point_cloud)) # len(), not .size + geometry = obj.geometries.geometries[0] + label = geometry.label + ``` + +- **Approach offset worked; learner practices the grasp offset.** Walk through the approach pose fully: `approach_pose = offset_pose(obj_in_world.pose, APPROACH_MM)` (`APPROACH_MM = 100`). Then have the learner compute the grasp offset themselves; the answer is `grasp_pose = offset_pose(obj_in_world.pose, GRIPPER_LENGTH_MM)` (`GRIPPER_LENGTH_MM = 60`, the gripper-TCP-to-fingertip depth). Include the `offset_pose` helper. +- **`motion.move("gripper-1", …)` semantics explicit:** it drives the **gripper** coordinate frame to the destination world pose — NOT the arm end (which is what the UI MoveToPosition / the arm component `move_to_position` do). Contrast the two so the offset math makes sense. +- **Full pick loop** (hybrid: `motion.move` for the Cartesian pick, saved switches for the place), verbatim: + + ```python + await motion.move( + component_name=GRIPPER_NAME, + destination=PoseInFrame(reference_frame="world", pose=approach_pose), + ) + await gripper.open() + await motion.move( + component_name=GRIPPER_NAME, + destination=PoseInFrame(reference_frame="world", pose=grasp_pose), + ) + await gripper.grab() + await asyncio.sleep(0.3) + await travel.set_position(2) + await place_pose.set_position(2) + await gripper.open() + await home.set_position(2) + ``` + + Mention the optional straight-down descent follow-up: `Constraints(linear_constraint=[LinearConstraint(line_tolerance_mm=5.0)])` on the grasp move. +- **Debugging guide:** symptom → **3D scene tab** (what to look for), with a back-link to Phase 3 obstacle/safety-wall config (skipping it bites here). Home-pose guard is entry #1. +- **Granular sub-checkpoints:** detector works → transform yields sane world coords → approach reachable → grasp succeeds → full loop completes. + +**Verification:** run the four checks. `grep -n "point_cloud.size\|red-bin\|move_to_position(" …/05-perception-guided-picking.md` → expect zero (use `len(...)`, single bin, gripper-frame `motion.move`). `grep -n "get_object_point_clouds\|transform_pose\|GRIPPER_LENGTH_MM" …` → expect hits. + +**Commit:** `docs(tutorials): author Phase 5 perception-guided picking` + +--- + +### Task 8: Author Phase 6 — `06-inline-module.md` (optional; corrected API) + +**Goal:** Package the script as an inline module, with the **corrected** in-module `RobotClient` pattern. + +**Files:** Modify `docs/tutorials/pick-and-place/06-inline-module.md` + +**Spec:** `pick-n-place-tutorial-plan.md` → "`06-inline-module.md`"; `tutorial-review-notes.md` → Phase 6. + +**Content requirements:** + +- **Framed as optional** (no time pressure). **Strong "why bother":** you'd want this when it must survive disconnection, auto-restart, OTA deploy, or run on a schedule. +- **Honest framing:** "mostly packaging + one real change" — the `transform_pose` access genuinely changes; don't let a "same logic, different entry point" line set a trap. +- **Tier the scope:** MVP (repackage + `do_command`, trigger manually) is the core optional path; scheduled jobs + autonomous operation are an explicit "level 2." +- Inline module editor walkthrough; `validate_config` + `reconfigure` (dependency injection). +- **CORRECTED `transform_pose` inside a module** — there is **no** `FrameSystemClient` and no injected frame-system dependency. Create a **single, reused `RobotClient`** from env vars. Verbatim: + + ```python + import os + from viam.robot.client import RobotClient + + async def create_robot_client_from_module(): + opts = RobotClient.Options.with_api_key( + api_key=os.environ["VIAM_API_KEY"], + api_key_id=os.environ["VIAM_API_KEY_ID"], + ) + return await RobotClient.at_address(os.environ["VIAM_MACHINE_FQDN"], opts) + + # self.robot_client initialized to None; create once, reuse: + if not self.robot_client: + self.robot_client = await create_robot_client_from_module() + world_pose = await self.robot_client.transform_pose(obj_in_cam, "world") + ``` + + Note: exactly one client, reused; do NOT create a connection per call; do NOT hardcode credentials (operator sets `VIAM_API_KEY`, `VIAM_API_KEY_ID`, `VIAM_MACHINE_FQDN` in the module's environment config). Close it on shutdown with `await self.robot_client.close()`. Reference: +- **Bridge callout:** side-by-side `from_robot` (local script) vs. `cast + get_resource_name` (module); resource names are identical in both. +- **`do_command` + scheduled job.** Cloud build time (~1 min for Python modules) stated upfront. + +**Verification:** run the four checks. `grep -n "FrameSystemClient" docs/tutorials/pick-and-place/06-inline-module.md` → **zero**. `grep -n "VIAM_MACHINE_FQDN\|create_robot_client_from_module" …` → hits. + +**Commit:** `docs(tutorials): author Phase 6 inline module with corrected in-module RobotClient` + +--- + +### Task 9: Final consistency sweep + full build + +**Goal:** Cross-page consistency and a clean full-site build. + +**Files:** all of `docs/tutorials/pick-and-place/` + +**Step 1: Cross-page grep sweep (expect zero hits each)** + +```bash +cd /Users/nick.hehr/src/viam-docs +grep -rniE "FrameSystemClient|point_cloud\.size|facilitator provides|pre-provisioned by instructor|sorted by color|red-bin|blue-bin|green-bin|does not exist yet|phase_total: 5|local-python-script" docs/tutorials/pick-and-place/ +``` + +**Step 2: Confirm the six-phase chain** + +```bash +grep -rn "phase:\|phase_total:\|prev:\|next:" docs/tutorials/pick-and-place/*.md +``` + +Verify: phases 1–6, all `phase_total: 6`, prev/next form the chain platform-mental-model → configure-resources → static-positions → control-the-robot-from-python → perception-guided-picking → inline-module. + +**Step 3: The four pre-PR checks (verbatim, in order)** + +```bash +cd /Users/nick.hehr/src/viam-docs +npx prettier --write "docs/tutorials/pick-and-place/**/*.md" +npx markdownlint-cli --config .markdownlint.yaml "docs/tutorials/pick-and-place/**/*.md" +vale sync && vale docs/tutorials/pick-and-place/ +make build-prod +``` + +Expected: prettier reformats in place; markdownlint clean; vale reports no errors; `make build-prod` completes without errors (old-date warnings OK). + +**Step 4: Browser spot-check (optional but recommended)** + +```bash +pkill -f "hugo server"; rm -rf public/ +hugo server --port 1313 --disableFastRender +``` + +Open each phase: `workshop-phases` box and sidebar show six phases in order with the current one highlighted; `workshop-nav` prev/next chains across all six; the `/tutorials/` landing card and `/tutorials/all/` archive are unaffected. + +**Step 5: Commit any sweep fixes** + +```bash +git add docs/tutorials/pick-and-place/ +git commit -m "docs(tutorials): final consistency sweep for six-phase self-serve workshop" +``` + +--- + +## Out of scope (deferred) + +- Companion-repo code changes (e.g. adding the home-pose guard clause to `reference-solution.py`). +- The hardware-setup how-to guide (`docs/guides/hardware-setup/xarm6-pick-and-place.md`). +- The `code-file` shortcode and `data/tutorials.yaml`. +- Header/hardware-overview imagery. +- Any change to the landing page, `/tutorials/all/` archive, or sidebar layout (2026-06-30 work). From 327b7d0cb148a335ae313eaac5d12f70f49507fd Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 10:09:56 -0400 Subject: [PATCH 32/44] refactor(tutorials): restructure pick-and-place to six phases Split perception into Phase 5, rename Phase 4 to control-the-robot-from-python, renumber inline module to Phase 6, bump phase_total, rewire prev/next. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01B26gLpfchSJJMgUg6rCFKS --- .../01-platform-mental-model.md | 2 +- .../pick-and-place/02-configure-resources.md | 2 +- .../pick-and-place/03-static-positions.md | 4 +- .../04-control-the-robot-from-python.md | 43 ++++++++++++++ .../pick-and-place/04-local-python-script.md | 56 ------------------- .../05-perception-guided-picking.md | 46 +++++++++++++++ ...5-inline-module.md => 06-inline-module.md} | 16 +++--- docs/tutorials/pick-and-place/_index.md | 13 +++-- .../pick-and-place/_phase-template.md | 2 +- 9 files changed, 109 insertions(+), 75 deletions(-) create mode 100644 docs/tutorials/pick-and-place/04-control-the-robot-from-python.md delete mode 100644 docs/tutorials/pick-and-place/04-local-python-script.md create mode 100644 docs/tutorials/pick-and-place/05-perception-guided-picking.md rename docs/tutorials/pick-and-place/{05-inline-module.md => 06-inline-module.md} (82%) diff --git a/docs/tutorials/pick-and-place/01-platform-mental-model.md b/docs/tutorials/pick-and-place/01-platform-mental-model.md index cf1d482686..37941df1b5 100644 --- a/docs/tutorials/pick-and-place/01-platform-mental-model.md +++ b/docs/tutorials/pick-and-place/01-platform-mental-model.md @@ -8,7 +8,7 @@ description: "Understand how the Viam cloud, agent, and server fit together befo workshop: "pick-and-place" toc_hide: true phase: 1 -phase_total: 5 +phase_total: 6 time_estimate: "15 minutes" next: "/tutorials/pick-and-place/configure-resources/" languages: ["python"] diff --git a/docs/tutorials/pick-and-place/02-configure-resources.md b/docs/tutorials/pick-and-place/02-configure-resources.md index 09ba241ca9..d43c16dbef 100644 --- a/docs/tutorials/pick-and-place/02-configure-resources.md +++ b/docs/tutorials/pick-and-place/02-configure-resources.md @@ -8,7 +8,7 @@ description: "Tour the pre-configured resources, the color-detection vision pipe workshop: "pick-and-place" toc_hide: true phase: 2 -phase_total: 5 +phase_total: 6 time_estimate: "20 minutes" prev: "/tutorials/pick-and-place/platform-mental-model/" next: "/tutorials/pick-and-place/static-positions/" diff --git a/docs/tutorials/pick-and-place/03-static-positions.md b/docs/tutorials/pick-and-place/03-static-positions.md index 23a4b1c0ed..bd0c740dc3 100644 --- a/docs/tutorials/pick-and-place/03-static-positions.md +++ b/docs/tutorials/pick-and-place/03-static-positions.md @@ -8,10 +8,10 @@ description: "Save the arm's key poses and configure WorldState obstacles, provi workshop: "pick-and-place" toc_hide: true phase: 3 -phase_total: 5 +phase_total: 6 time_estimate: "20 minutes" prev: "/tutorials/pick-and-place/configure-resources/" -next: "/tutorials/pick-and-place/local-python-script/" +next: "/tutorials/pick-and-place/control-the-robot-from-python/" languages: ["python"] --- diff --git a/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md b/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md new file mode 100644 index 0000000000..a7c7f099e8 --- /dev/null +++ b/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md @@ -0,0 +1,43 @@ +--- +title: "Phase 4: Control the robot from Python" +linkTitle: "4. Control from Python" +type: "docs" +slug: "control-the-robot-from-python" +weight: 40 +description: "Connect from your laptop and drive the saved static pick-and-place sequence from a Python script." +workshop: "pick-and-place" +toc_hide: true +phase: 4 +phase_total: 6 +time_estimate: "15 minutes" +prev: "/tutorials/pick-and-place/static-positions/" +next: "/tutorials/pick-and-place/perception-guided-picking/" +languages: ["python"] +--- + +In this phase you write and run a Python script on your laptop that connects to the robot and executes the static pick-and-place sequence from Phase 3. This proves your connection, environment, and named positions work end to end before you add perception in Phase 5. + +{{< workshop-phases >}} + +## Why a script before a module + + + +## Check your environment + + + + +## Connect to your robot + + + +## Run the static sequence + + + +## Debugging guide + + + +{{< workshop-nav >}} diff --git a/docs/tutorials/pick-and-place/04-local-python-script.md b/docs/tutorials/pick-and-place/04-local-python-script.md deleted file mode 100644 index 712cdfb3c8..0000000000 --- a/docs/tutorials/pick-and-place/04-local-python-script.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: "Phase 4: Local Python script" -linkTitle: "4. Local Python script" -type: "docs" -slug: "local-python-script" -weight: 40 -description: "Connect from your laptop, run the static sequence, then add perception to complete the pick-and-sort loop." -workshop: "pick-and-place" -toc_hide: true -phase: 4 -phase_total: 5 -time_estimate: "30 minutes" -prev: "/tutorials/pick-and-place/static-positions/" -next: "/tutorials/pick-and-place/inline-module/" -languages: ["python"] ---- - -This is the core phase of the workshop: you write and run a Python script on your laptop that connects to the robot, executes the static pick-and-place sequence from Phase 3, and then adds live color detection to sort cubes autonomously. - -{{< workshop-phases >}} - -## Why a script before a module - - - -## Check your environment - - - - -## Connect to your robot - - - -## Run the static sequence - - - -## The frame system and transforms - - - -## Add perception - - - - -## Pass obstacles to the motion service - - - -## Debugging guide - - - -{{< workshop-nav >}} diff --git a/docs/tutorials/pick-and-place/05-perception-guided-picking.md b/docs/tutorials/pick-and-place/05-perception-guided-picking.md new file mode 100644 index 0000000000..71571984ae --- /dev/null +++ b/docs/tutorials/pick-and-place/05-perception-guided-picking.md @@ -0,0 +1,46 @@ +--- +title: "Phase 5: Perception-guided picking" +linkTitle: "5. Perception-guided picking" +type: "docs" +slug: "perception-guided-picking" +weight: 50 +description: "Add the vision pipeline and write the perception loop that detects a block, transforms it to the world frame, and picks it with motion planning." +workshop: "pick-and-place" +toc_hide: true +phase: 5 +phase_total: 6 +time_estimate: "22 minutes" +prev: "/tutorials/pick-and-place/control-the-robot-from-python/" +next: "/tutorials/pick-and-place/inline-module/" +languages: ["python"] +--- + +In this phase you replace the fixed grasp pose from Phase 4 with live perception: the vision service detects a block, the frame system transforms its position into world space, and the motion service plans a collision-free pick. + +{{< workshop-phases >}} + +## Configure the vision pipeline + + + +## The frame system and transform_pose + + + +## Detect from home (the wrist-camera rule) + + + +## Compute the approach and grasp poses + + + +## Run the full pick loop + + + +## Debugging guide + + + +{{< workshop-nav >}} diff --git a/docs/tutorials/pick-and-place/05-inline-module.md b/docs/tutorials/pick-and-place/06-inline-module.md similarity index 82% rename from docs/tutorials/pick-and-place/05-inline-module.md rename to docs/tutorials/pick-and-place/06-inline-module.md index 6c3c50113e..7f4ed3c115 100644 --- a/docs/tutorials/pick-and-place/05-inline-module.md +++ b/docs/tutorials/pick-and-place/06-inline-module.md @@ -1,20 +1,20 @@ --- -title: "Phase 5: Inline module" -linkTitle: "5. Inline module" +title: "Phase 6: Inline module" +linkTitle: "6. Inline module" type: "docs" slug: "inline-module" -weight: 50 +weight: 60 description: "Optional: package your working script as an inline module that runs on the robot." workshop: "pick-and-place" toc_hide: true -phase: 5 -phase_total: 5 -time_estimate: "15 minutes" -prev: "/tutorials/pick-and-place/local-python-script/" +phase: 6 +phase_total: 6 +time_estimate: "20 minutes" +prev: "/tutorials/pick-and-place/perception-guided-picking/" languages: ["python"] --- -This phase is optional: if you want the pick-and-sort cycle to run on the robot without a laptop connection, you can package your Phase 4 script as an inline module using the Viam app's built-in editor. +This phase is optional: if you want the pick-and-sort cycle to run on the robot without a laptop connection, you can package your Phase 5 script as an inline module using the Viam app's built-in editor. {{< workshop-phases >}} diff --git a/docs/tutorials/pick-and-place/_index.md b/docs/tutorials/pick-and-place/_index.md index daaf1c65ca..f8f9f06447 100644 --- a/docs/tutorials/pick-and-place/_index.md +++ b/docs/tutorials/pick-and-place/_index.md @@ -23,7 +23,7 @@ companion_repo: "https://github.com/viam-devrel/pick-and-place" no_list: true --- -In this workshop you build a vision-guided robot that detects colored cubes, picks each one up, and drops it in the correct bin, sorted by color. The workshop is structured as five sequential phases, each ending with a working system state you can verify before moving on. Completing Phase 4 (the local Python script) is a full success; Phase 5 (packaging your script as an inline module) is optional. +In this workshop you build a vision-guided robot that detects colored cubes, picks each one up, and drops it in the correct bin, sorted by color. The workshop is structured as six sequential phases, each ending with a working system state you can verify before moving on. Completing Phase 4 (the local Python script) is a full success; Phase 5 (packaging your script as an inline module) is optional. ## What you'll build @@ -40,11 +40,12 @@ You will configure an xArm6 robotic arm fitted with a finger gripper and an Inte ## Phases -1. **[Platform mental model](/tutorials/pick-and-place/platform-mental-model/)** (~15 min): learn how Viam resources, the frame system, and the motion service fit together before touching any hardware. -2. **[Configure resources and explore the app](/tutorials/pick-and-place/configure-resources/)** (~20 min): add the arm, gripper, camera, and vision service in the Viam app; confirm each resource is live. -3. **[Static positions and safety obstacles](/tutorials/pick-and-place/static-positions/)** (~20 min): define named arm poses and add a virtual obstacle to prevent the arm from hitting the table. -4. **[Local Python script](/tutorials/pick-and-place/local-python-script/)** (~30 min): write and run the pick-and-sort script from your laptop; this is the core goal of the workshop. -5. **[Inline module](/tutorials/pick-and-place/inline-module/)** (~15 min, optional): package the script as an on-robot module so the cycle runs without a laptop connection. +1. **[Platform mental model](/tutorials/pick-and-place/platform-mental-model/)** (~15 min) +2. **[Configure resources and explore the app](/tutorials/pick-and-place/configure-resources/)** (~20 min) +3. **[Static positions and safety obstacles](/tutorials/pick-and-place/static-positions/)** (~20 min) +4. **[Control the robot from Python](/tutorials/pick-and-place/control-the-robot-from-python/)** (~15 min) — milestone one +5. **[Perception-guided picking](/tutorials/pick-and-place/perception-guided-picking/)** (~22 min) — milestone two +6. **[Inline module](/tutorials/pick-and-place/inline-module/)** (~20 min, optional) ## Prerequisites diff --git a/docs/tutorials/pick-and-place/_phase-template.md b/docs/tutorials/pick-and-place/_phase-template.md index 66e7fb0ba5..78471ca116 100644 --- a/docs/tutorials/pick-and-place/_phase-template.md +++ b/docs/tutorials/pick-and-place/_phase-template.md @@ -8,7 +8,7 @@ description: "One-sentence description of this phase." workshop: "pick-and-place" toc_hide: true phase: 0 -phase_total: 5 +phase_total: 6 time_estimate: "NN minutes" prev: "/tutorials/pick-and-place/previous-slug/" next: "/tutorials/pick-and-place/next-slug/" From 08167f34f71f5852387aeed074a4cb89cdac6952 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 10:17:42 -0400 Subject: [PATCH 33/44] docs(tutorials): rewrite pick-and-place overview for self-serve (milestones, prerequisites gate) --- docs/tutorials/pick-and-place/_index.md | 49 +++++++++++++++++-------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/docs/tutorials/pick-and-place/_index.md b/docs/tutorials/pick-and-place/_index.md index f8f9f06447..93d02aae0d 100644 --- a/docs/tutorials/pick-and-place/_index.md +++ b/docs/tutorials/pick-and-place/_index.md @@ -3,7 +3,7 @@ title: "Vision-Guided Pick-and-Place with the xArm6" linkTitle: "Pick-and-Place Workshop" type: "docs" weight: 50 -description: "Build a robot that detects, picks, and sorts colored cubes with vision and motion planning, from static poses to a Python script to an optional module." +description: "Build a robot that detects blocks by shape and sorts them into a bin with computer vision and motion planning, from static poses to a Python perception loop to an optional module." authors: [] level: "Intermediate" languages: ["python"] @@ -23,45 +23,64 @@ companion_repo: "https://github.com/viam-devrel/pick-and-place" no_list: true --- -In this workshop you build a vision-guided robot that detects colored cubes, picks each one up, and drops it in the correct bin, sorted by color. The workshop is structured as six sequential phases, each ending with a working system state you can verify before moving on. Completing Phase 4 (the local Python script) is a full success; Phase 5 (packaging your script as an inline module) is optional. +In this workshop you build a vision-guided robot that finds blocks by shape and picks each one into a bin. A `shape-detector` vision service locates blocks in camera space and feeds a `vision-segment` service (model `detections-to-segments`) that turns each detection into a point cloud segment the motion planner can grasp. + +The workshop is structured as six sequential phases, each ending with a working system state you can verify before moving on. The workshop has two milestones: by the end of Phase 4 you drive the robot from your own code through a static, pre-planned sequence (milestone one, a real and bankable win), and by the end of Phase 5 you close the loop with live perception so the robot detects, picks, and places blocks on its own (milestone two). At minimum, aim to complete the Phase 4 script. ## What you'll build -You will configure an xArm6 robotic arm fitted with a finger gripper and an Intel RealSense depth camera. A color-detection vision service identifies cube positions in camera space; the Viam motion service plans and executes collision-free arm movements to pick each cube and place it in the right bin. By the end of Phase 4 you have a Python script you can run from your laptop that drives the full pick-and-sort cycle autonomously. +You will configure an xArm6 robotic arm fitted with a finger gripper and a wrist-mounted Intel RealSense depth camera. A shape-detection vision service finds blocks in camera space, and the Viam motion service plans and executes collision-free picks that place each block in the bin. By the end of Phase 5 you have a Python script you run from your laptop that drives the full detect-pick-place loop. ## Hardware -- **uFactory xArm6**: the six-axis robotic arm that picks and places the cubes. -- **Intel RealSense D435**: the depth camera mounted to detect cube positions and colors. -- **uFactory finger gripper**: the end-effector that grasps the cubes. +- **uFactory xArm6**: the six-axis robotic arm that picks and places the blocks. +- **Intel RealSense D435**: the wrist-mounted depth camera that detects block positions by shape. +- **uFactory finger gripper**: the end-effector that grasps the blocks. - **System76 Meerkat**: the on-robot mini-PC that runs the Viam machine server. ## Phases +Phases 1 through 5 are the core workshop. Phase 6 is optional. + 1. **[Platform mental model](/tutorials/pick-and-place/platform-mental-model/)** (~15 min) 2. **[Configure resources and explore the app](/tutorials/pick-and-place/configure-resources/)** (~20 min) 3. **[Static positions and safety obstacles](/tutorials/pick-and-place/static-positions/)** (~20 min) -4. **[Control the robot from Python](/tutorials/pick-and-place/control-the-robot-from-python/)** (~15 min) — milestone one -5. **[Perception-guided picking](/tutorials/pick-and-place/perception-guided-picking/)** (~22 min) — milestone two +4. **[Control the robot from Python](/tutorials/pick-and-place/control-the-robot-from-python/)** (~15 min, milestone one) +5. **[Perception-guided picking](/tutorials/pick-and-place/perception-guided-picking/)** (~22 min, milestone two) 6. **[Inline module](/tutorials/pick-and-place/inline-module/)** (~20 min, optional) ## Prerequisites -**Hardware pre-provisioned for you:** if you are in a guided workshop where the hardware is already set up, skip directly to [Phase 1](/tutorials/pick-and-place/platform-mental-model/). +This is a self-serve workshop, so confirm each of the following before you start: -**Provisioning your own hardware:** complete the hardware setup guide first (forthcoming), then return here for [Phase 1](/tutorials/pick-and-place/platform-mental-model/). The setup guide covers mounting the camera, connecting the arm controller, and installing viam-server on the Meerkat. +- **Python 3.10 or newer.** Install it with [uv](https://docs.astral.sh/uv/getting-started/installation/) (recommended) or from [python.org](https://www.python.org/downloads/). +- **The Viam Python SDK.** The companion `scripts/` project already declares `viam-sdk`, so `uv run` installs it for you in Phase 4. See the [Python SDK docs](https://python.viam.dev/) for reference. Pip works too if you prefer it. +- **A working terminal** on the machine you will run the Phase 4 and Phase 5 scripts from, typically your laptop rather than the robot's Meerkat. +- **A Viam account with an accessible machine.** Log in at [app.viam.com](https://app.viam.com), open your machine, and confirm the green **Live** indicator before you begin. -Before Phase 4 you also need Python 3.10 or newer and the Viam Python SDK on the machine you will run the script from. Verify with: +### Validate your environment + +Before starting Phase 4, confirm your environment is ready: ```sh -python3 --version # 3.10 or newer -python3 -c "import viam" # must succeed +python3 --version # 3.10 or newer +uv run --with viam-sdk python -c "import viam; print(viam.__version__)" # prints a version ``` +If either command fails, revisit the checklist above before continuing. + +### Where to start + +- **Physical hardware ready:** start at [Phase 1](/tutorials/pick-and-place/platform-mental-model/). +- **Provisioning your own hardware:** complete the hardware setup guide first (forthcoming), then return here for [Phase 1](/tutorials/pick-and-place/platform-mental-model/). + +Only the physical hardware, viam-agent, and viam-server may be pre-provisioned for you. Configuring the arm, gripper, camera, and vision and motion services is always your hands-on work in this workshop, starting in Phase 2. + ## Companion code -All supporting files for this workshop live in the [viam-devrel/pick-and-place](https://github.com/viam-devrel/pick-and-place) repository. It contains the machine config fragment you will import in Phase 2, the starter script for Phase 4, and the reference solution for Phase 5. +All supporting files for this workshop live in the [viam-devrel/pick-and-place](https://github.com/viam-devrel/pick-and-place) repository. - +- `config/` holds a machine config fragment for each phase. Use it to check your work after you configure resources by hand, not as something to import wholesale. +- `scripts/` holds the starter script for Phase 4 and the reference solution for Phase 5. From de9a66cc2f764ffd4b81e225dc52f4ae42a25071 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 10:27:09 -0400 Subject: [PATCH 34/44] docs(tutorials): author Phase 1 platform mental model (self-serve) --- .../01-platform-mental-model.md | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/docs/tutorials/pick-and-place/01-platform-mental-model.md b/docs/tutorials/pick-and-place/01-platform-mental-model.md index 37941df1b5..436bc5f730 100644 --- a/docs/tutorials/pick-and-place/01-platform-mental-model.md +++ b/docs/tutorials/pick-and-place/01-platform-mental-model.md @@ -20,30 +20,79 @@ Before you configure a single resource, this phase gives you the mental map you ## Three questions to answer first - +By the end of this phase you should be able to answer three questions. Keep them in mind as you read, and check yourself against them again at the end. + +1. What are the three layers of a Viam machine, and which one does your Python code actually talk to? +2. What is the difference between a component and a service? +3. Why does adding the arm in Phase 2 trigger a download? + +You will not write any code or change any configuration in this phase. Instead, you will open your own machine in the Viam app and look at what is already there, so that the concepts below have something real to point at. ## The three layers - +A Viam machine is made of three layers that each do one job: + +- **The Viam cloud app** is the source of truth for configuration. When you add a component, change an attribute, or wire up a service, you are editing a JSON document stored in the cloud. The app never runs your robot directly; it describes what should run. +- **viam-agent** runs on the machine itself. It manages the install: it installs, updates, and keeps `viam-server` running, restarts it if it crashes, and provides the initial bootstrap credentials viam-server needs to reach the cloud. Think of viam-agent as the process supervisor, not as the source of your resource config, and not as something you interact with directly during this workshop. +- **viam-server** is the process that does the actual work. It pulls its resource config from the cloud app, starts every component and service that config describes, and exposes them over an API. This is the layer that drives the arm, reads the camera, and runs the vision pipeline. + +Open your machine's overview page in the Viam app now and find the status indicator that shows the machine is live. That indicator reflects viam-agent keeping viam-server running and connected to the cloud app, the handoff between all three layers happening continuously in the background. ## How your SDK connects - +In Phase 4 you write a Python script that imports the Viam SDK and connects to your machine. That connection goes to `viam-server`, not to the cloud app. The cloud app helps your script locate the machine and authenticate, but once the connection is established, every control API call goes directly to `viam-server` on the robot: moving the arm, reading the camera, calling the vision service. + +This matters because it explains why your script keeps working even if your laptop briefly loses its connection to the internet at large but keeps a path to the robot: the cloud app is not in the request path for control, only for discovery and configuration delivery. + +Open the **CONNECT** tab on your machine's page in the Viam app and look at the code sample it generates. The address and API key shown there are exactly what your Phase 4 script will use to reach `viam-server`. ## Configuration is the source of truth - +Whatever you set in the app's **CONFIGURE** tab is what the machine runs. There is no separate step to "deploy" a change: when you save an edit, the cloud app updates the config document, and `viam-server` picks up the new config and applies it, typically within seconds, without a restart for most changes. + +This is why the workshop asks you to make changes in the app rather than by editing a file on the robot directly. The app's CONFIGURE tab is the only place you need to look to know what a machine will do. + +Open the **CONFIGURE** tab now and find the JSON view toggle near the top of the panel. Switching to JSON shows you the exact document that `viam-server` receives, the same resources you see as cards in the builder view, expressed as the config it consumes. ## Resources: the universal abstraction - +Everything a Viam machine does, hardware and software alike, is modeled as a **resource**. Each resource has a name you choose (like `arm-1`), an API that describes what kind of thing it is (an arm, a camera, a vision service), and a model that identifies the specific implementation. + +Open the **CONFIGURE** tab and find `arm-1`. Next to its name you will see a triplet in the form `namespace:family:model`, for example `viam:ufactory:xArm6`. That triplet tells `viam-server` exactly which code to run for this resource: who published it (`namespace`), what family of hardware it belongs to (`family`), and the specific model (`model`). + +The same idea applies to `gripper-1` and `cam-1`: each is a resource with a name, an API, and a model, even though one drives a gripper and the other reads a depth camera. ## Components and services - +Resources split into two kinds: + +- **Components** represent physical hardware. `arm-1`, `gripper-1`, and `cam-1` are all components: each one wraps a piece of hardware and exposes a standard API for it (an arm API with move commands, a camera API that returns images, and so on). +- **Services** represent software capabilities that consume other resources rather than hardware directly. A motion service plans collision-free paths for an arm. Later, in Phase 5, you configure a `shape-detector` vision service that reads frames from `cam-1` and finds blocks by shape, and a `vision-segment` service (model `detections-to-segments`) that takes those detections and turns them into point cloud segments the motion planner can grasp. Neither service is a physical thing; each one composes other resources into a new capability. + +Open the **CONTROL** tab and look at the cards laid out for your machine. Each card corresponds to one resource. The arm, gripper, and camera cards let you jog hardware directly; any service card lets you exercise a capability that is built on top of that hardware. + +{{< alert title="Foreshadowing" color="note" >}} +You will not configure `shape-detector` or `vision-segment` until Phase 5. For now, just notice the pattern: a service is defined by what other resources it depends on, not by hardware it owns. +{{< /alert >}} + +## Builtin resources and modules + +Some resources are **builtin**: `viam-server` (also called the RDK, the robot development kit) ships with support for common APIs and a handful of default models out of the box. Most of the interesting functionality on a real machine, though, comes from **modules**: packages that `viam-server` downloads and runs to add support for a specific model. + +The `viam:ufactory:xArm6` arm you saw in the CONFIGURE tab is module-provided. In Phase 2, the moment you add that arm to your config, `viam-server` recognizes it does not have `viam:ufactory:xArm6` built in, downloads the module that provides it from the Viam registry, and starts it. You will be able to watch that download and start happen live in the LOGS tab. + +Open the **CONFIGURE** tab again and compare `arm-1`'s namespace to the namespace on any resource marked `rdk` (if you see one). A namespace of `rdk` means the model ships inside `viam-server` itself; any other namespace, like `viam` or `erh`, means the model arrives as a module. ## The resource dependency graph - +Resources can depend on each other. A gripper attaches to an arm, so `gripper-1`'s config points at `arm-1`. A vision service reads from a camera, so it depends on `cam-1`. `viam-server` reads these dependencies out of your config and builds a graph, then starts resources in an order that respects it: a resource cannot start until everything it depends on has started. + +This is the same pattern you will see again in Phase 5: `vision-segment` depends on `shape-detector`, which depends on `cam-1`. Neither can produce meaningful output until the resource beneath it is running. + +Open the **LOGS** tab and look at the startup sequence the next time the machine restarts (you do not need to trigger one now). The order resources come online in the log follows the dependency graph, not the order they appear in the config file. + +{{< alert title="Check yourself" color="note" >}} +Before moving to Phase 2, make sure you can answer the three questions from the top of this page: name the three layers and which one your code talks to, what separates a component from a service, and why adding the arm triggers a module download. If any answer feels shaky, re-skim the relevant section above. +{{< /alert >}} {{< workshop-nav >}} From baebfe8392efacabf34c0435d1aeda026e3f9f8d Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 10:28:47 -0400 Subject: [PATCH 35/44] fix(tutorials): trim pick-and-place overview description under SEO length Regression from the self-serve overview rewrite; keep it under 158 chars. --- docs/tutorials/pick-and-place/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/pick-and-place/_index.md b/docs/tutorials/pick-and-place/_index.md index 93d02aae0d..62e14e138a 100644 --- a/docs/tutorials/pick-and-place/_index.md +++ b/docs/tutorials/pick-and-place/_index.md @@ -3,7 +3,7 @@ title: "Vision-Guided Pick-and-Place with the xArm6" linkTitle: "Pick-and-Place Workshop" type: "docs" weight: 50 -description: "Build a robot that detects blocks by shape and sorts them into a bin with computer vision and motion planning, from static poses to a Python perception loop to an optional module." +description: "Build a vision-guided robot that detects blocks by shape and sorts them into a bin with motion planning, from static arm poses to a Python perception loop." authors: [] level: "Intermediate" languages: ["python"] From f223a967fe0e0f2d1c1608f161bb18d8fc3a0f97 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 10:38:26 -0400 Subject: [PATCH 36/44] docs(tutorials): author Phase 2 hands-on resource configuration --- .../pick-and-place/02-configure-resources.md | 91 +++++++++++++++++-- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/docs/tutorials/pick-and-place/02-configure-resources.md b/docs/tutorials/pick-and-place/02-configure-resources.md index d43c16dbef..c15b0f7e4c 100644 --- a/docs/tutorials/pick-and-place/02-configure-resources.md +++ b/docs/tutorials/pick-and-place/02-configure-resources.md @@ -4,7 +4,7 @@ linkTitle: "2. Configure resources" type: "docs" slug: "configure-resources" weight: 20 -description: "Tour the pre-configured resources, the color-detection vision pipeline, and the control and scene tabs." +description: "Configure the arm, gripper, and camera by hand, then verify each one from the control and 3D scene tabs." workshop: "pick-and-place" toc_hide: true phase: 2 @@ -15,28 +15,99 @@ next: "/tutorials/pick-and-place/static-positions/" languages: ["python"] --- -In this phase you open the pre-loaded machine configuration, verify that each resource is live, and get familiar with the Viam app tools you will use throughout the rest of the workshop. +In this phase you configure the three hardware resources every pick-and-place machine needs: an arm, a gripper, and a camera. Your machine starts with none of them. You add each one by hand, watch it come online, and confirm it works from the CONTROL tab before moving to the next. {{< workshop-phases >}} -## What is already configured +## The target state - +By the end of this phase your CONFIGURE tab holds three components: -## The vision pipeline +| Name | API | Model | +| ----------- | ----------------------- | ----------------------- | +| `arm-1` | `rdk:component:arm` | `viam:ufactory:xArm6` | +| `gripper-1` | `rdk:component:gripper` | `viam:ufactory:gripper` | +| `cam-1` | `rdk:component:camera` | `viam:camera:realsense` | - +You will not touch the vision service in this phase. The `shape-detector` and `vision-segment` services that let the machine find blocks by shape come later, in Phase 5, right before you write the code that calls them. For now, focus on the hardware. -## Explore the control tab +## Configure the arm - +On the **CONFIGURE** tab, add a new component. In the add-component dialog, search for `xArm6` and select the `viam:ufactory:xArm6` result. Name the component `arm-1`. + +Set the following attributes: + +- `host`: the xArm controller's IP address, provided by your workshop facilitator +- `port`: `502` + +You can leave `speed_degs_per_sec` and `acceleration_degs_per_sec_per_sec` unset for now; both are optional and default to safe values. + +Save the config. This is the moment from Phase 1 made concrete: `viam-server` does not have `viam:ufactory:xArm6` built in, so it fetches the `viam:ufactory` module from the Viam registry and starts it. Open the **LOGS** tab and watch it happen: a log line for the module download, then one for the module starting, then `arm-1` itself coming online. The whole sequence usually takes well under a minute. + +{{< alert title="Two components, one module" color="note" >}} +The `viam:ufactory` module provides both the arm model and the gripper model you configure next. You only pay the download cost once. +{{< /alert >}} + +## Configure the gripper + +Add another component. In the add-component dialog, search for the `viam:ufactory:gripper` model and select it. This model comes from the same `viam:ufactory` module as the arm. Name it `gripper-1`. + +Set one attribute: + +- `arm`: `"arm-1"` + +This attribute is also a dependency: `gripper-1` cannot start until `arm-1` is running, because the gripper is physically mounted on the arm and controlled through the same connection. Save the config and check the **LOGS** tab again. Because `viam-server` already has the `viam:ufactory` module running from the previous step, `gripper-1` comes online immediately with no second download. + +## Configure the camera + +Add a third component. In the add-component dialog, search for `realsense` and select the `viam:camera:realsense` result. Name it `cam-1`. + +Set the following attributes: + +- `sensors`: `["color", "depth"]` +- `width_px`: `640` +- `height_px`: `480` + +Save the config and confirm in the **LOGS** tab that `viam-server` downloads the `viam:camera-realsense` module and starts `cam-1`. This is a separate module from `viam:ufactory`, since it comes from a different publisher and family. + +## Test each resource from the CONTROL tab + +Open the **CONTROL** tab. You should now see a test card for each of the three components you just added. + +On the camera card, confirm you get a live feed: + +{{< checkpoint >}} +The camera card shows a live color stream from `cam-1`. If depth is available as a separate view, confirm that stream updates too. If the card is blank, check the LOGS tab for a camera error before moving on. +{{< /checkpoint >}} + +On the arm card, jog a joint with the sliders and confirm the arm moves. Then select **Get end position** and confirm it returns x, y, and z coordinates. + +{{< checkpoint >}} +Jogging a joint slider moves the physical arm, and **Get end position** returns a coordinate rather than an error. If jogging does nothing, confirm `arm-1` shows as online in the CONFIGURE tab and that the LOGS tab has no connection errors for it. +{{< /checkpoint >}} + +On the gripper card, place a block between the gripper fingers by hand, then select **Grab**. The fingers should close and hold the block. Select **Open** and confirm the fingers release it. {{< checkpoint >}} -TODO: confirm the camera, arm, and vision test cards each return data. +With a block between the fingers, **Grab** closes the fingers and the gripper holds the block without dropping it. **Open** releases the block. This grab-and-release is the same action your Python code performs later in the workshop when it picks a block and drops it in a bin. If your gripper card also shows a holding status indicator, it now reads true while the block is held and false once the gripper is open and empty. {{< /checkpoint >}} ## The 3D scene tab - +Open the **3D scene** tab. You should see a rendered model of the arm and the `cam-1` frame positioned at the end of its wrist. + +Jog joint 1 with the arm card's sliders and watch the 3D scene as the arm turns. The `cam-1` frame moves with the arm, because the camera is mounted on the wrist rather than fixed in the workspace. + +{{< alert title="Why this matters later" color="note" >}} +Because the camera is wrist-mounted, every detection it makes is relative to wherever the arm happens to be pointed at that moment. In Phase 5 you will detect from a single known pose so that "camera space" always means the same thing. Notice that pose here; you will meet it again as the "detect from home" rule. +{{< /alert >}} + +The exact camera mounting offset used to transform its frame into the arm's frame comes from your hardware setup guide or the pre-provisioned config, not from anything you configure in this phase. For now, just confirm the relationship: the frame exists, and it tracks the arm. + +## Check your work + +If you want to compare your configuration against a known-good version, the companion repo has a reference copy at [machine-fragment.json](https://github.com/viam-devrel/pick-and-place/blob/main/config/machine-fragment.json). Use it to check your work, not to import over what you just built by hand. + +With `arm-1`, `gripper-1`, and `cam-1` all live and verified, you are ready for Phase 3, where you save a set of fixed arm poses and prove the full hardware sequence works before perception enters the picture. {{< workshop-nav >}} From 9c0e3a4d917e00e361118c676a222b027de7cec9 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 10:46:10 -0400 Subject: [PATCH 37/44] docs(tutorials): rewrite Phase 3 for self-serve (five poses, component obstacles, measured workspace) --- .../pick-and-place/03-static-positions.md | 201 ++++++++++++------ 1 file changed, 136 insertions(+), 65 deletions(-) diff --git a/docs/tutorials/pick-and-place/03-static-positions.md b/docs/tutorials/pick-and-place/03-static-positions.md index bd0c740dc3..4e8b305bb6 100644 --- a/docs/tutorials/pick-and-place/03-static-positions.md +++ b/docs/tutorials/pick-and-place/03-static-positions.md @@ -4,7 +4,7 @@ linkTitle: "3. Static positions" type: "docs" slug: "static-positions" weight: 30 -description: "Save the arm's key poses and configure WorldState obstacles, proving the hardware and motion planning work before you add perception." +description: "Save the arm's key poses and configure obstacle components, proving the hardware and motion planning work before you add perception." workshop: "pick-and-place" toc_hide: true phase: 3 @@ -15,15 +15,15 @@ next: "/tutorials/pick-and-place/control-the-robot-from-python/" languages: ["python"] --- - - In this phase you move the arm through the full pick-and-place sequence using saved poses, with no perception yet. The goal is to prove that the hardware and motion planning work before you add detection, so that any bug you encounter later has only one new cause. {{< workshop-phases >}} ## 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. Once the arm reliably travels through every stage of the sequence, perception becomes the only new variable when you move to Phase 4. +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 Phase 4 you drive this same proven sequence from a Python script, and perception does not enter the picture until Phase 5. Once the arm reliably travels through every stage of the sequence, perception becomes the only new variable when you reach it. + +This is not just a classroom shortcut. 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. The table below shows what each step in the static sequence validates: @@ -34,34 +34,46 @@ The table below shows what each step in the static sequence validates: | Arm reaches grasp pose | Descent distance is correct | | Gripper opens and closes | Finger timing is right | | Arm reaches travel pose | Safe carrying height clears obstacles | -| Arm reaches bin pose | Bin position is correct | +| Arm reaches place pose | Bin position is correct | ## The key poses Each saved pose has a specific role in the sequence: -| Pose | Purpose | -| ---------------- | ---------------------------------------------------------------------------------- | -| home-pose | Observation position above the workspace; the camera has a clear view of all cubes | -| approach-pose | Directly above the pick zone, roughly 80 to 100 mm above the highest cube | -| grasp-pose | At the cube with the gripper open; fingertips are level with the cube top | -| travel-pose | Safe carrying height that clears bins and table edges while holding a cube | -| [color]-bin-pose | Above each target sorting bin; one pose per color (red, blue, green) | +| Pose | Purpose | +| ------------- | ----------------------------------------------------------------------------------------- | +| home-pose | Observation position above the workspace; the wrist camera has a clear view of the blocks | +| approach-pose | Standoff directly above the pick zone, roughly 80 to 100 mm above the highest block | +| grasp-pose | At the block, gripper open and ready to close; fingertips are level with the block top | +| travel-pose | Safe carrying height that clears obstacles while holding a block | +| place-pose | Above the bin where blocks are dropped | 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. ## Save each pose with the arm position saver -The machine is already configured with `arm-position-saver` switch components (model `erh:vmodutils:arm-position-saver`), one per pose. Each switch has an `arm` attribute pointing to `arm-1`. If you are setting up your own machine rather than using the pre-loaded configuration, add the `erh:vmodutils` module from the Viam registry and add a switch component for each pose. +You configure pose saving by hand, the same way you configured the arm, gripper, and camera in Phase 2. + +Add the `erh:vmodutils` module from the Viam registry to your machine. This module provides the `erh:vmodutils:arm-position-saver` switch model you use to save and recall poses, and the `erh:vmodutils:obstacle` model you use later in this phase. + +Add a **switch** component for `home-pose`: -For each pose, follow these steps: +- API: `rdk:component:switch` +- Model: `erh:vmodutils:arm-position-saver` +- 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. + +With the `home-pose` switch added, save and verify it: 1. Open the **CONTROL** tab and find the arm test card. Use the joint sliders to jog the arm into position. 2. Select **Get end position** on the arm card and note the x, y, and z values to confirm the arm is where you expect it. -3. On the pose's switch test card, set the switch to **position 1** to save the current joint positions. +3. On the switch test card, set the switch to **position 1** to save the current joint positions. 4. Set the switch to **position 2** to confirm the arm returns to the saved pose from any starting position. -Repeat this process for home-pose, approach-pose, grasp-pose, travel-pose, red-bin-pose, blue-bin-pose, and green-bin-pose. +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 **Get end position**, set the switch to position 1 to save, and set it to position 2 to confirm the arm returns. {{< alert title="Switch positions" color="note" >}} On an `arm-position-saver` switch, position 1 saves the current joint positions, position 2 moves the arm to the saved pose, and position 0 clears any saved data. Always save with position 1 before you attempt position 2. Setting position 2 on an unsaved switch does nothing. @@ -71,79 +83,138 @@ On an `arm-position-saver` switch, position 1 saves the current joint positions, Set each saved switch to position 2 in turn. The arm should move to each pose you saved. 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. {{< /checkpoint >}} -## Teach the planner about obstacles with WorldState +## 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. -The Viam motion planner is collision-aware, but it can only avoid geometry it knows about. Without a WorldState configuration, the planner avoids self-collisions only. When you add WorldState, the planner treats the table surface, the sorting bins, and any workspace boundary walls as hard obstacles it cannot plan through. +This matters both for correctness and for safety. Without the table obstacle, the planner might find a path that swings the arm through the table surface. Virtual safety walls at the workspace boundary also prevent the arm from swinging into people standing nearby. This is not just a classroom convenience: it is the same pattern you would use to keep a production workcell's motion planner honoring the real boundaries of its cell. -This matters both for correctness and for safety. Without the table obstacle, the planner might find a path that swings the arm through the table surface. Without bin obstacles, it might carry a cube directly through a bin wall. Virtual obstacle walls at the workspace boundary also prevent the arm from swinging into people standing nearby. +In this workshop you configure two categories of obstacle: the table surface and two safety walls at the workspace boundary. Bin geometry is out of scope for this phase. -For this workshop you define three categories of obstacle: the table surface, one box per sorting bin, and safety walls at the workspace boundary. +## Obstacles are components -## Measure and configure the obstacles +Obstacles are not a separate WorldState file you import. Each obstacle is an `erh:vmodutils:obstacle` component you add on the **CONFIGURE** tab, the same way you added the arm, gripper, and camera. The obstacle model uses the gripper API, so once configured, each obstacle shows up in `resource_names` as a gripper. That is expected; the model reuses the gripper API purely as a resource container for geometry, it does not add a real gripper to your machine. -Your workshop facilitator provides the table and bin dimensions. To find each bin's position relative to the arm base, move the arm over the center of the bin and read the x and y values from **Get end position**. The z coordinate for a box obstacle is the center of the box, not its top surface. +### Measure your workspace -The following JSON shows the structure for the obstacles array. Add this to the WorldState configuration for your motion service: +Before you can fill in the obstacle geometry, measure your own table and workspace boundary. You need two kinds of measurement, and each one feeds a different part of the config: - +- **Tape-measure dimensions** for the box sizes: the table's length, width, and thickness go into the table obstacle's `x`, `y`, and `z`. Use the tape measure for how big each box is, not for where it sits. +- **Arm-relative positions** for the box translations: jog the arm to a landmark, such as the front edge of the table or the side boundary, and select **Get end position** on the arm's CONTROL card to read the x and y coordinates in the arm's coordinate frame. These are the numbers that fill the `REPLACE_WITH_MEASURED_FRONT_Y` and `REPLACE_WITH_MEASURED_SIDE_X` placeholders in the safety walls below. + +All obstacle geometry is expressed against the world origin, which in this setup sits at the arm base, so the x and y coordinates you read from **Get end position** drop straight into the `translation` fields without any conversion. + +Each obstacle geometry is a box defined by its `x`, `y`, and `z` dimensions (the box's full size in millimeters) and a `translation` (the box's center point in the parent frame). Two details trip people up: + +- The `z` value in a box's dimensions is its height, but the `translation.z` you provide is the box's **center**, not its top or bottom surface. A 30 mm thick table sitting flush with the world origin at `z = 0` needs a `translation.z` of `-15`, half the thickness, because the box extends from `-30` to `0` and its center is at `-15`. +- The safety walls are thin, tall boxes. A wall that is 600 mm tall has `z: 600` for its dimension, but its `translation.z` is `300`, half the height, for the same reason. + +### Add the table obstacle + +Add a component: + +- API: `rdk:component:gripper` +- Model: `erh:vmodutils:obstacle` ```json { - "obstacles": [ - { - "label": "table", - "geometries": [ - { - "type": "box", - "x": 1200, - "y": 800, - "z": 30, - "translation": { - "x": 0, - "y": 0, - "z": -15 - } - } - ] - }, - { - "label": "red-bin", - "geometries": [ - { - "type": "box", - "x": 200, - "y": 200, - "z": 150, - "translation": { - "x": "[measured]", - "y": "[measured]", - "z": 75 - } - } - ] - } - ] + "name": "table", + "api": "rdk:component:gripper", + "model": "erh:vmodutils:obstacle", + "attributes": { + "geometries": [ + { + "label": "table", + "type": "box", + "x": 1200, + "y": 800, + "z": 30, + "translation": { "x": 0, "y": 0, "z": -15 }, + "parent": "world" + } + ] + } } ``` -The table z translation is -15 because the box is 30 mm thick and its center sits 15 mm below the world origin at z = 0. Each bin's z translation is half its height because the box center is at the midpoint of the bin walls. Replace `[measured]` with the x and y values you read from the arm when centered over each bin, then add a matching entry for blue-bin and green-bin. +Replace the `x`, `y`, and `z` dimensions with your own measured table length, width, and thickness. If your table is not centered on the arm base in x and y, adjust `translation.x` and `translation.y` to match, using the values you read from **Get end position** when jogging to the table edges. + +### Add the safety walls -You can import a ready-made starting point from the companion repo: [obstacles-template.json](https://github.com/viam-devrel/pick-and-place/blob/main/config/obstacles-template.json). The full machine configuration, including all pose switches, is in [machine-fragment.json](https://github.com/viam-devrel/pick-and-place/blob/main/config/machine-fragment.json). +Add two more `erh:vmodutils:obstacle` components, one per boundary you want to wall off: + +```json +{ + "name": "safety-wall-front", + "api": "rdk:component:gripper", + "model": "erh:vmodutils:obstacle", + "attributes": { + "geometries": [ + { + "label": "safety-wall-front", + "type": "box", + "x": 1200, + "y": 20, + "z": 600, + "translation": { + "x": 0, + "y": "REPLACE_WITH_MEASURED_FRONT_Y", + "z": 300 + }, + "parent": "world" + } + ] + } +} +``` + +```json +{ + "name": "safety-wall-side", + "api": "rdk:component:gripper", + "model": "erh:vmodutils:obstacle", + "attributes": { + "geometries": [ + { + "label": "safety-wall-side", + "type": "box", + "x": 20, + "y": 800, + "z": 600, + "translation": { + "x": "REPLACE_WITH_MEASURED_SIDE_X", + "y": 0, + "z": 300 + }, + "parent": "world" + } + ] + } +} +``` + +Replace `REPLACE_WITH_MEASURED_FRONT_Y` and `REPLACE_WITH_MEASURED_SIDE_X` with the coordinates you measured for the front and side boundaries of your workspace. Both walls are 600 mm tall, so their `translation.z` is 300, half the height. + +You can check your obstacle configuration against the companion repo's [obstacles-template.json](https://github.com/viam-devrel/pick-and-place/blob/main/config/obstacles-template.json), which has the full set with example measurements filled in. The full machine configuration, including all pose switches, is in [machine-fragment.json](https://github.com/viam-devrel/pick-and-place/blob/main/config/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: ```text -home-pose (2) -> approach-pose (2) -> open gripper -> -grasp-pose (2) -> close gripper -> travel-pose (2) -> -red-bin-pose (2) -> open gripper -> home-pose (2) +home-pose (2) -> approach-pose (2) -> Open gripper -> +grasp-pose (2) -> Grab -> travel-pose (2) -> +place-pose (2) -> Open gripper -> home-pose (2) ``` -As the arm moves, open the **3D scene** tab and watch the arm's path to confirm it does not intersect the table surface or bin walls. If the path clips an obstacle boundary, the planner will report a collision error. Open the **LOGS** tab alongside the 3D scene to monitor for motion planning errors in real time. +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 >}} -The arm completes the full sequence without stopping, the gripper opens and closes at the correct moments, and the LOGS tab shows no collision errors. If planning fails at a step, open the 3D scene tab to see what geometry the planner sees. Adjust the pose or the obstacle dimensions and retry. A common cause is a bin obstacle that is positioned slightly off center, causing the planner to see the arm path as intersecting the bin wall even though the physical arm clears it. +The arm completes the full sequence without stopping, the gripper opens and closes at the correct moments, and the LOGS tab shows no collision errors. If planning fails at a step, open the 3D scene tab to see what geometry the planner sees. Adjust the pose or the obstacle dimensions and retry. A common cause is an obstacle that is positioned slightly off from its physical counterpart, causing the planner to see the arm path as intersecting geometry that the physical arm actually clears. {{< /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. + {{< workshop-nav >}} From 3226eed0590ff2bc8073acb9fddc3dd7d5dbab83 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 13:48:25 -0400 Subject: [PATCH 38/44] docs(tutorials): author Phase 4 driving the static sequence from Python --- .../04-control-the-robot-from-python.md | 123 ++++++++++++++++-- 1 file changed, 115 insertions(+), 8 deletions(-) diff --git a/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md b/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md index a7c7f099e8..647415d89c 100644 --- a/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md +++ b/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md @@ -21,23 +21,130 @@ In this phase you write and run a Python script on your laptop that connects to ## Why a script before a module - +Everything you did in Phase 3 happened by clicking test cards on the **CONTROL** tab. That is a fine way to verify hardware, but it does not scale: you cannot loop, branch on a sensor reading, or retry a failed grasp from a button. A local Python script gives you those things, plus fast iteration, a local debugger, and the ability to sprinkle in `print` statements wherever you need visibility into what the robot is doing. -## Check your environment +A script is also the right starting point before you package anything as a module. A module has to satisfy a defined lifecycle and run inside `viam-server`, which makes it slower to iterate on and harder to debug. In Phase 6 you package this same logic as a module. For now, a script you run from your own terminal, that you can stop, edit, and rerun in seconds, is the faster path to a working pick-and-place loop. - - +This phase is worth the detour because of what it buys you. Every control you clicked in Phases 2 and 3 maps directly onto an SDK method: the arm card's joint sliders become `Arm` methods, **Grab** and **Open** on the gripper card become `gripper.grab()` and `gripper.open()`, and setting a switch to position 2 becomes `switch.set_position(2)`. Once you can call those methods from code, you can compose them into logic no UI card lets you express by clicking. + +## Get the companion project + +The workshop's companion repository, [viam-devrel/pick-and-place](https://github.com/viam-devrel/pick-and-place), has a `scripts/` project with a starter script already set up for this phase. Clone or download the repository, then work from the `scripts/` directory: + +```sh +git clone https://github.com/viam-devrel/pick-and-place.git +cd pick-and-place/scripts +``` + +Your environment was already validated in the workshop prerequisites, so this phase is about connecting and running, not debugging Python installs. Run the starter script with `uv`, the primary path for this workshop: + +```sh +uv run python starter-script.py +``` + +`uv` reads the project's `pyproject.toml` and `.python-version` and resolves `viam-sdk` for you automatically, so there is no separate install step. If you are not using `uv`, pip works as a fallback once you have installed the project's dependencies yourself: + +```sh +pip install viam-sdk +python3 starter-script.py +``` ## Connect to your robot - +Open `starter-script.py` and find the `connect()` function. It mirrors the boilerplate the Viam app generates for you on the machine's **CONNECT** tab, under **Python SDK**: + +```python +MACHINE_ADDRESS = "" +API_KEY = "" +API_KEY_ID = "" + +async def connect() -> RobotClient: + return await RobotClient.at_address( + MACHINE_ADDRESS, + options=RobotClient.Options.with_api_key( + api_key=API_KEY, + api_key_id=API_KEY_ID, + ), + ) +``` + +Open the **CONNECT** tab on your machine's page in the Viam app, select **Python SDK**, and copy the three values it shows you: the machine address and an API key and key ID pair. Paste them into `MACHINE_ADDRESS`, `API_KEY`, and `API_KEY_ID` at the top of the script. You are reading and understanding this boilerplate rather than writing it from scratch, the same connection code every Viam Python script starts with. + +{{< alert title="Handle your API key like a secret" color="note" >}} +Your API key grants control of the robot to anyone who has it. Do not commit it to version control. The companion repo's `.gitignore` already excludes the starter script's typical edit locations, but the safer pattern is to read the key from an environment variable instead of pasting it directly into the file, for example `API_KEY = os.environ["VIAM_API_KEY"]`. +{{< /alert >}} + +## Get typed resource handles + +After the connection opens, the script builds typed handles for each resource you drive in this phase: + +```python +arm = Arm.from_robot(machine, "arm-1") +gripper = Gripper.from_robot(machine, "gripper-1") + +home = Switch.from_robot(machine, "home-pose") +approach = Switch.from_robot(machine, "approach-pose") +grasp = Switch.from_robot(machine, "grasp-pose") +travel = Switch.from_robot(machine, "travel-pose") +place_pose = Switch.from_robot(machine, "place-pose") +``` -## Run the static sequence +Phase 4 drives only the arm, the gripper, and these pose switches. The starter script also declares two more handles right next to these: a `motion` handle for the `builtin` motion service, which always exists on a machine, and a `vision` handle for the `vision-segment` service. You do not configure `vision-segment` until Phase 5, and `VisionClient.from_robot` raises a `ResourceNotFoundError` when the service it names is not present. Because of that, the `vision = VisionClient.from_robot(...)` line must not run yet. - +Before you run the script, make sure that line is not active. If it is uncommented, comment it out for now: + +```python +# vision = VisionClient.from_robot(machine, "vision-segment") +``` + +You enable it in Phase 5 once the vision service exists. The `motion` handle is safe to leave as it is, since the `builtin` motion service is always present, but nothing in this phase calls it either. + +## Run the script + +Run the script now with `uv run python starter-script.py`. It happens in a single run: `connect()` opens the connection, the script prints every resource on the machine, and then it immediately drives the arm through the static sequence. Watch the printed resource list scroll past in your terminal before the arm starts moving. + +The first thing printed is the full resource list: + +```python +print(machine.resource_names) +``` + +{{< checkpoint >}} +`machine.resource_names` prints a list that includes at least `arm-1`, `gripper-1`, and `cam-1`, the five poses (`home-pose`, `approach-pose`, `grasp-pose`, `travel-pose`, `place-pose`) as switches, and the three obstacles from Phase 3 as grippers. The list also contains the `builtin` motion service and other `erh:vmodutils` entries, so expect more names than just these. Seeing the obstacles listed as grippers is expected: the `erh:vmodutils:obstacle` model reuses the gripper API purely as a resource container for geometry. +{{< /checkpoint >}} + +Right after the print, the script runs the static sequence. This is the same sequence you tested by hand from the **CONTROL** tab at the end of Phase 3, now expressed as code instead of button clicks. On a switch, `set_position(2)` executes the pose it has saved: + +```python +await home.set_position(2) +await approach.set_position(2) +await gripper.open() +await grasp.set_position(2) +await gripper.grab() +await asyncio.sleep(0.3) # finger gripper settle +await travel.set_position(2) +await place_pose.set_position(2) +await gripper.open() +await home.set_position(2) +``` + +The short sleep after `gripper.grab()` gives the finger gripper time to settle its grip on the block before the arm starts moving again; without it, the arm can begin the travel move before the fingers have finished closing. + +Notice that nothing in this code mentions the table or the safety walls. The obstacles you configured in Phase 3 live in the machine config, not in this script, and the motion system applies them automatically wherever planning happens. There is no runtime `WorldState` to build or pass in here. In this static phase, movement comes entirely from the saved-pose switches, so obstacle-aware planning is not something you will see kick in yet; it becomes visible once Phase 5 introduces planned moves toward a detected block. + +{{< checkpoint >}} +After the resource list prints, the same run drives the arm through the full sequence end to end: home, approach, open, grasp, grab, travel, place, open, home. The arm should complete every step without stopping, in the same order you validated manually in Phase 3. +{{< /checkpoint >}} ## Debugging guide - +Most Phase 4 problems fall into one of a few categories: + +- **`ResourceNotFoundError: vision-segment`** (or a similar not-found error for the vision service) means you tried to build the vision handle before configuring the vision service. That service is not added until Phase 5. Comment out the `vision = VisionClient.from_robot(...)` line for now, as described above, and rerun. +- **Connection failures.** If `connect()` raises an error or hangs, double-check the `MACHINE_ADDRESS`, `API_KEY`, and `API_KEY_ID` values against the **CONNECT** tab. A stale or mistyped API key produces an authentication error immediately; a wrong address usually times out instead. Also confirm the machine shows the green **Live** indicator in the Viam app. A machine that is not live cannot accept a connection no matter how correct your credentials are. +- **Resource-name mismatches.** If `Arm.from_robot(machine, "arm-1")` or a similar call raises a not-found error, the name in your script does not match the name on the **CONFIGURE** tab. Names are exact strings, not approximations, so `arm-1` and `arm_1` are different resources as far as the SDK is concerned. Open `resource_names` from the connection checkpoint above and compare it character for character against the names your script uses. +- **A switch does nothing on `set_position(2)`.** This means the pose was never saved. Go back to the **CONTROL** tab and set that switch to position 1 to save the current arm position, as you did in Phase 3, then rerun the script. + +With `resource_names` printing everything you expect and the static sequence running end to end from your own code, you have working proof that your connection, your named resources, and your saved poses all hold up under real code, not just button clicks. In Phase 5 you replace the fixed `grasp-pose` in this sequence with a position computed from live perception, so the arm picks whichever block the camera actually detects instead of always reaching for the same spot. {{< workshop-nav >}} From 7fbe00c1d4ed8430389188fe1ac9892e30b7022c Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 14:03:03 -0400 Subject: [PATCH 39/44] docs(tutorials): author Phase 5 perception-guided picking Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01B26gLpfchSJJMgUg6rCFKS --- .../05-perception-guided-picking.md | 210 +++++++++++++++++- 1 file changed, 204 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/pick-and-place/05-perception-guided-picking.md b/docs/tutorials/pick-and-place/05-perception-guided-picking.md index 71571984ae..27f1b4237e 100644 --- a/docs/tutorials/pick-and-place/05-perception-guided-picking.md +++ b/docs/tutorials/pick-and-place/05-perception-guided-picking.md @@ -21,26 +21,224 @@ In this phase you replace the fixed grasp pose from Phase 4 with live perception ## Configure the vision pipeline - +Phase 2 previewed two vision services and asked you to hold off configuring them. Add both now, in order, since the second depends on the first. + +Add a **vision** service: + +- API: `rdk:service:vision` +- Model: `devrel:shape-finder:detector` +- Name: `shape-detector` + +```json +{ + "name": "shape-detector", + "api": "rdk:service:vision", + "model": "devrel:shape-finder:detector", + "attributes": { + "camera_name": "cam-1" + } +} +``` + +The `camera_name` attribute is also a dependency: `shape-detector` cannot run until `cam-1` is online, the same dependency pattern you have already seen with `gripper-1` and `arm-1`. This service reads color frames from `cam-1` and finds blocks by shape, in two dimensions, with no depth information yet. + +Add a second **vision** service: + +- API: `rdk:service:vision` +- Model: `viam:vision:detections-to-segments` +- Name: `vision-segment` + +```json +{ + "name": "vision-segment", + "api": "rdk:service:vision", + "model": "viam:vision:detections-to-segments", + "attributes": { + "detector_name": "shape-detector", + "confidence_threshold_pct": 0.5, + "mean_k": 50, + "sigma": 1.5 + } +} +``` + +`vision-segment` depends on `shape-detector`, the same graph relationship as before: it takes each 2D shape detection, pulls the matching depth points from `cam-1`, filters noisy points out with the `mean_k` and `sigma` attributes, and fuses the result into a 3D object point cloud per detected block. The `confidence_threshold_pct` value is a fraction from 0 to 1, not a percentage figure, so `0.5` means keep detections at 50 percent confidence or higher. A 2D detection alone cannot tell you how far away a block is or where it sits in space; `vision-segment` is what turns "a block-shaped region of pixels" into "a block at this point in three dimensions." + +Save the config and open the **CONTROL** tab. Find the `vision-segment` test card and run it against `cam-1`. You should see one or more segmented objects, each drawn as a point cloud with a bounding box, over the shapes sitting in front of the camera. + +{{< checkpoint >}} +The `vision-segment` test card returns at least one object when a block is in view. If it returns nothing, confirm a block actually sits in the camera's field of view, then check the `shape-detector` card on its own: if that also returns nothing, the problem is upstream in shape detection, not in the depth fusion step. +{{< /checkpoint >}} + +With the service live, go back to `starter-script.py` and uncomment the vision handle you commented out in Phase 4: + +```python +vision = VisionClient.from_robot(machine, "vision-segment") +``` + +`VisionClient.from_robot` raised a `ResourceNotFoundError` in Phase 4 because `vision-segment` did not exist yet. Now that it is configured and online, the same line resolves. ## The frame system and transform_pose - +Every pose that `vision-segment` returns is expressed in the `cam-1` frame. That is the only frame the vision service knows about: it looked at pixels and depth values coming out of one camera, so the coordinates it hands back describe where a block sits relative to that camera's own origin and orientation. + +The motion service does not think in camera coordinates. It plans in the `world` frame, the same frame your obstacle geometry in Phase 3 was defined against. To hand a detected pose to the motion service, you first have to express it in `world` instead of `cam-1`. + +This is the frame system you already saw in Phase 2's 3D scene tab, when the `cam-1` frame visibly moved as you jogged joint 1. `viam-server` maintains that same relationship as a graph: the camera's offset from the wrist, the wrist's offset from the next joint, and so on, all the way down to the arm's base at the world origin. `RobotClient.transform_pose` walks that graph for you. Give it a pose tagged with its source frame and a destination frame, and it returns the equivalent pose in the destination frame: + +```python +obj_in_cam = PoseInFrame(reference_frame="cam-1", pose=geometry.center) +obj_in_world = await machine.transform_pose(obj_in_cam, "world") +``` + +`PoseInFrame` pairs a `Pose` with the name of the frame it is expressed in. `transform_pose` reads that source frame, reads the destination frame you passed as the second argument, and returns a new `PoseInFrame` with the same physical point re-expressed in `world` coordinates. You will wire this into the full detection code in the next section, once there is an actual `geometry.center` to transform. ## Detect from home (the wrist-camera rule) - +The `cam-1` frame is only a fixed thing to reason about when the arm is in a fixed pose. Because the camera is wrist-mounted, jogging any joint changes where `cam-1` sits in space, which changes what `transform_pose` reports for the exact same physical block. If you detect once with the arm near the bin and again with the arm hovering over the table, `transform_pose` gives two different world answers for two different arm positions, even if no block has moved at all. + +The fix is a rule, not a calculation: always move to `home-pose`, the observation position from Phase 3, before you call the vision service. Every detection in this workshop starts from the same known arm position, so `cam-1` always means the same thing when `transform_pose` reads it. + +Enforce the rule structurally, as a guard clause at the top of every pick attempt, rather than trusting yourself to remember it: + +```python +# Observe from home so the wrist-mounted camera frame is in a known position. +await home.set_position(2) + +objects = await vision.get_object_point_clouds("cam-1") +if not objects: + print("No objects detected") + return False + +# Largest object by point count. Use len(point_cloud), NOT .size. +obj = max(objects, key=lambda o: len(o.point_cloud)) +geometry = obj.geometries.geometries[0] +label = geometry.label +print(f"Detected: {label}") + +# The object pose is in the camera frame; the planner needs world frame. +obj_in_cam = PoseInFrame(reference_frame="cam-1", pose=geometry.center) +obj_in_world = await machine.transform_pose(obj_in_cam, "world") +``` + +`get_object_point_clouds` returns one entry per object `vision-segment` fused together, each carrying its own point cloud and geometry. A workspace with several blocks in view returns several entries, so you need a rule for which one to pick this cycle. `max(objects, key=lambda o: len(o.point_cloud))` picks the object with the most points, ordinarily the block closest to the camera or most fully in view. Use `len(o.point_cloud)` here, not a `.size` attribute; a point cloud in the Python SDK is a plain sequence of points, and `len()` is how you count them. + +Add a `print(obj_in_world.pose)` after the transform and run the script. Watch the x, y, and z values it prints as you move a block around the table. + +{{< checkpoint >}} +`obj_in_world.pose` prints coordinates that make physical sense: a `z` roughly at the table surface plus the block's height, and `x`/`y` values that land somewhere over the table rather than off in empty space or underneath it. If the numbers look physically wrong, the most common cause is a detection that was not taken from `home-pose`. Confirm the guard clause runs before every `get_object_point_clouds` call. +{{< /checkpoint >}} ## Compute the approach and grasp poses - +Before you turn `obj_in_world.pose` into a place to move the gripper, it matters exactly what `motion.move` moves. Two motions that sound similar are not the same thing: + +- The CONTROL tab's arm card, and a direct `Arm` method, move the arm's own end frame: the flange at the end of the last joint. +- `motion.move(component_name="gripper-1", ...)` moves the `gripper-1` frame instead, the gripper's own tool center point (TCP), which sits further down the kinematic chain than the arm's end because the gripper is bolted on past it. + +Every offset you compute in this section is an offset from `obj_in_world.pose` to wherever you want the `gripper-1` frame to end up, not the arm's end. Keep that distinction in mind or the math below will not make sense. + +The workshop's `offset_pose` helper raises or lowers a pose in `z` while leaving `x`, `y`, and orientation untouched: + +```python +def offset_pose(pose: Pose, z_offset_mm: float) -> Pose: + """Raise or lower a pose in z while keeping x/y/orientation fixed.""" + return Pose( + x=pose.x, + y=pose.y, + z=pose.z + z_offset_mm, + o_x=pose.o_x, + o_y=pose.o_y, + o_z=pose.o_z, + theta=pose.theta, + ) +``` + +The approach pose is a standoff directly above the block, high enough that the gripper can descend onto it without first colliding with it sideways. That offset is worked for you: + +```python +approach_pose = offset_pose(obj_in_world.pose, APPROACH_MM) +``` + +`APPROACH_MM` is 100. Since every offset here is applied to `obj_in_world.pose`, which is the block's bounding-box center, this places the standoff 100 mm above the block center: enough clearance for the gripper to descend cleanly, with room to spare for a small pose error. + +Now compute the grasp pose yourself. What you actually want at the block is the gripper's fingers, not its TCP: the fingers have to close around the block. The `gripper-1` TCP frame sits one gripper-length above the fingertip contact point, so to put the fingers at the block center you place the TCP one gripper-length (`GRIPPER_LENGTH_MM`) above it, not at it. Remember that `motion.move` is already driving the gripper's own TCP, not the arm's end, so this is the only offset you add here; you do not account for the whole arm reach again. Work out the offset before reading on. + +The offset is `GRIPPER_LENGTH_MM`, the depth from the gripper's TCP down to its fingertip contact point: + +```python +grasp_pose = offset_pose(obj_in_world.pose, GRIPPER_LENGTH_MM) +``` + +`GRIPPER_LENGTH_MM` is 60. If you used `APPROACH_MM` here by mistake, the gripper stops well above the block instead of at it; if you used zero, you would drive the TCP itself to the block center, sinking the fingers a full gripper-length past the block instead of closing them around it. ## Run the full pick loop - +With `approach_pose` and `grasp_pose` computed, assemble the full cycle: + +```python +await motion.move( + component_name="gripper-1", + destination=PoseInFrame(reference_frame="world", pose=approach_pose), +) +await gripper.open() +await motion.move( + component_name="gripper-1", + destination=PoseInFrame(reference_frame="world", pose=grasp_pose), +) +await gripper.grab() +await asyncio.sleep(0.3) # finger gripper settle +await travel.set_position(2) +await place_pose.set_position(2) +await gripper.open() +await home.set_position(2) +``` + +Notice the shape of this cycle: it picks with `motion.move` and places with the saved-pose switches from Phase 3. That split is deliberate, not an inconsistency. The pick target moves every cycle, so it needs the Cartesian precision and obstacle-aware planning that `motion.move` provides against a freshly computed world pose. The place target never moves: it is the same bin in the same spot every time, so a pre-measured, pre-verified saved pose is simpler and just as reliable as planning a fresh path there. Use the right tool for each half of the cycle rather than forcing one approach to do both jobs. + +Run the script and watch the sequence in stages: the approach move first, then the grasp move, then the full cycle through to a placed block. + +{{< checkpoint >}} +The approach move completes without a planning error, positioning the gripper above the block. If it fails here, open the **3D scene** tab during the next run and check whether `approach_pose` lands inside the table or safety-wall geometry from Phase 3; a block detected very close to a boundary can push the standoff outside the planner's reachable space. +{{< /checkpoint >}} + +{{< checkpoint >}} +The grasp move completes and **Grab** closes the fingers around the block, holding it through the lift into `travel-pose`. If the gripper closes on empty air, the block likely shifted between detection and grasp, or the grasp offset is off; revisit the offset math above. +{{< /checkpoint >}} + +{{< checkpoint >}} +The full loop runs end to end: approach, open, grasp, grab, travel, place, open, home, with the block landing in the bin. This is the same sequence you drove by hand in Phase 3 and by fixed poses in Phase 4, now driven by a pose your code computed from a live detection. +{{< /checkpoint >}} + +{{< alert title="Optional: force a straight descent" color="note" >}} +The plan above lets the planner choose its own path from the approach pose down to the grasp pose, which is sometimes a shallow arc rather than a straight vertical drop. If you want the gripper to descend in a straight line instead, pass a `LinearConstraint` on the grasp move: + +```python +from viam.proto.service.motion import Constraints, LinearConstraint + +linear_down = Constraints( + linear_constraint=[LinearConstraint(line_tolerance_mm=5.0)] +) + +await motion.move( + component_name="gripper-1", + destination=PoseInFrame(reference_frame="world", pose=grasp_pose), + constraints=linear_down, +) +``` + +This is a refinement, not a requirement for a working pick loop. Try the unconstrained version first, and reach for this only if an arcing descent causes the gripper to clip a block on the way down. +{{< /alert >}} ## Debugging guide - +Work through these in order. The first one causes most of the rest. + +- **Did you detect from `home-pose`?** This is the first thing to check for nearly every perception symptom below. If the `await home.set_position(2)` guard is missing before a `get_object_point_clouds` call, or if you added a second detection somewhere that skips it, every downstream pose is computed against the wrong camera position. +- **No objects detected.** Open the **CONTROL** tab and run the `vision-segment` card by hand while a block sits in view. If that also returns nothing, check the `shape-detector` card on its own: a detector that finds nothing means the block is out of frame, or lighting has changed enough to affect the shape detection. If `shape-detector` finds the block but `vision-segment` does not, check that a block is close enough and clearly separated from the table surface for the depth fusion step to segment it. +- **The pick point drifts from cycle to cycle, even for a block that has not moved.** This is almost always the wrist-camera rule again: some code path is detecting from a pose other than `home-pose`. Print `obj_in_world.pose` on every cycle and confirm the arm is fully settled at `home-pose` before each detection call. +- **Motion planning fails, or the target looks unreachable.** Open the **3D scene** tab during the failing move and look at where `approach_pose` or `grasp_pose` lands relative to the table and safety-wall geometry from Phase 3. A detected pose near a workspace boundary can place the standoff or the grasp point outside the region the planner is allowed to move through. If you skipped or under-measured the obstacle configuration in Phase 3, this is where it bites: geometry that does not match your physical setup makes the planner reject moves that are perfectly safe, or, worse, accept ones that are not. Revisit [Teach the planner about obstacles](/tutorials/pick-and-place/static-positions/#teach-the-planner-about-obstacles) and recheck your measurements before assuming the pose math is wrong. + +With a full perception-guided pick loop running end to end, you have every piece of the workshop's core loop working from your own laptop: detection, the frame transform, planned motion, and a reliable place. Phase 6 is optional, and picks up from here to package this same script as a module that runs on the robot directly, with no laptop connection required once it is deployed. {{< workshop-nav >}} From dc7872289af8931ea00a2c131deb5e98c0cf678b Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 14:13:25 -0400 Subject: [PATCH 40/44] docs(tutorials): author Phase 6 inline module with corrected in-module RobotClient --- .../pick-and-place/06-inline-module.md | 126 ++++++++++++++++-- 1 file changed, 118 insertions(+), 8 deletions(-) diff --git a/docs/tutorials/pick-and-place/06-inline-module.md b/docs/tutorials/pick-and-place/06-inline-module.md index 7f4ed3c115..5dfb0321c7 100644 --- a/docs/tutorials/pick-and-place/06-inline-module.md +++ b/docs/tutorials/pick-and-place/06-inline-module.md @@ -14,25 +14,135 @@ prev: "/tutorials/pick-and-place/perception-guided-picking/" languages: ["python"] --- -This phase is optional: if you want the pick-and-sort cycle to run on the robot without a laptop connection, you can package your Phase 5 script as an inline module using the Viam app's built-in editor. +This phase is optional. Phase 5 already gave you a complete pick-and-place loop that runs from your own laptop: detection, the frame transform, planned motion, and a reliable place. This phase packages that same loop as a module so it runs on the robot directly, with no laptop connection required once it is deployed. {{< workshop-phases >}} -## Script versus module +## Why bother with a module - +A script you run from your laptop is a complete result. It is not a lesser version of a module, and nothing about Phase 5 was a placeholder waiting for this phase to finish it. Reach for a module only when one of these is true for your setup: -## The inline module editor +- The cycle has to keep running after you close your laptop or walk away. +- The cycle has to restart on its own if it crashes or the robot reboots. +- You want to deploy an updated version to the robot without pushing code from a laptop by hand. +- You want the cycle to run on a schedule instead of a manual trigger. - +If none of those apply, stop here. You have already built the thing this workshop set out to teach. + +## Mostly packaging, plus one real change + +Set expectations before you start: this phase is not a rewrite. The detection, the frame transform, the pose math, and the motion calls are the pick-and-place logic from Phase 5, moved into a module's lifecycle methods with no change to what they do. + +One piece of that logic does genuinely change, and it is worth calling out up front so it does not surprise you partway through: how you reach `transform_pose`. In Phase 5, `transform_pose` was a method on the `machine` handle your script already held from `RobotClient.at_address`. A module does not automatically receive that same handle. The corrected pattern for reaching `transform_pose` from inside a module is in [The frame system from inside a module](#the-frame-system-from-inside-a-module) below. Everything else in this phase is packaging. + +## Tier the scope + +Two tiers, so you can stop at whichever one matches what you came here for: + +- **A minimal viable module.** Repackage the Phase 5 logic into a module, and add a `do_command` handler you trigger by hand to run one pick-and-place cycle. This is the core path for the rest of this phase. +- **Level 2: scheduled and autonomous operation.** Once the minimal module runs a cycle on command, wiring it to a timer or a fully autonomous loop is a small additional step, covered briefly at the end of this phase as a low-effort on-ramp, not a requirement. + +## Open the inline module editor + +Before you start pasting code, know what to expect: saving an inline Python module triggers a cloud build, and that build takes about a minute. It is not instant the way rerunning a local script is, so give it that minute rather than assuming a save failed. + +Open your machine's **CONFIGURE** tab and add a new module. Choose to create a local module with an inline editor rather than pulling one from the registry, and select Python as the language. The Viam app opens a code editor in your browser with a generated module skeleton, so there is no local project setup to do first. + +Replace the skeleton's logic with your Phase 5 pick-and-place code: the connection to typed resource handles, the detection call, the frame transform, the pose math, and the motion calls, moved into the module's lifecycle methods as described below. Save the module. The Viam app packages your code and deploys it to the machine, and the **LOGS** tab shows the build progress the same way it showed module downloads back in Phase 2. + +{{< checkpoint >}} +The module finishes its cloud build and starts without errors in the **LOGS** tab, and its resource shows online on the **CONFIGURE** tab. If the build fails, read the build log for the specific error; a missing import or a syntax error carried over from the script is the most common cause. +{{< /checkpoint >}} ## Dependency injection - - +A script builds its resource handles once, right after it connects, by calling `Arm.from_robot(machine, "arm-1")` and similar for each resource it needs. A module does not connect to itself, so it cannot call `from_robot` the same way. Instead, the module framework hands your module its dependencies. + +Two lifecycle methods carry this pattern: + +- `validate_config` runs before your module starts and declares which resources it depends on, so `viam-server` knows to hold your module back until those resources are online, the same dependency ordering you already saw between `gripper-1` and `arm-1` in Phase 2. +- `reconfigure` receives the resolved dependencies as a mapping keyed by resource name, and this is where you build the typed handles your pick-and-place logic calls. + +A small illustrative sketch of that mapping, not a complete implementation: + +```python +from typing import cast +from viam.components.arm import Arm + +def reconfigure(self, config, dependencies): + self.arm = cast(Arm, dependencies[Arm.get_resource_name("arm-1")]) +``` + +Keep the rest of your `reconfigure` close to this shape: look up each resource your Phase 5 script used, cast it to its typed client, and store it on `self` so your pick-and-place logic can call it later. + +## The frame system from inside a module + +This is the one genuine change from Phase 5, so read it carefully even if you skim the rest of this phase. + +No injected dependency gives you frame-system access the way `dependencies[Arm.get_resource_name("arm-1")]` gives you the arm; there is no such dependency to inject. `transform_pose` lives on the machine-management API, and a module reaches that API the same way a script does: through a `RobotClient`. The difference is that a module has to build that `RobotClient` itself, from credentials in its own environment, rather than receiving one as a dependency. + +Use this pattern exactly as written: + +```python +import os +from viam.robot.client import RobotClient + +async def create_robot_client_from_module(): + opts = RobotClient.Options.with_api_key( + api_key=os.environ["VIAM_API_KEY"], + api_key_id=os.environ["VIAM_API_KEY_ID"], + ) + return await RobotClient.at_address(os.environ["VIAM_MACHINE_FQDN"], opts) + +# self.robot_client is initialized to None in __init__/reconfigure +# in logic, create once and reuse: +if not self.robot_client: + self.robot_client = await create_robot_client_from_module() +world_pose = await self.robot_client.transform_pose(obj_in_cam, "world") +``` + +Four rules go with this pattern: + +- Create exactly one `RobotClient` and reuse it. Do not open a new connection on every `do_command` call or every pick cycle; check `self.robot_client` first, the same way the snippet does, and only connect if it is not already set. +- Do not hardcode the API key, key ID, or machine address in your module's code. The operator sets `VIAM_API_KEY`, `VIAM_API_KEY_ID`, and `VIAM_MACHINE_FQDN` as environment variables in the module's configuration on the machine; they are not automatically injected the way component dependencies are. +- Close the connection on module shutdown by calling `await self.robot_client.close()`, the same cleanup discipline you would apply to any open connection. +- Everything else your module needs (the arm, the gripper, the vision service, the pose switches) still comes through the injected dependencies described above. `transform_pose` is the one exception, reached through this in-module `RobotClient` instead. + +See [Use the machine management API from a module](/build-modules/platform-apis/#use-the-machine-management-api-from-a-module) for the full reference on this pattern. + +{{< alert title="Same resource names, different retrieval" color="note" >}} +Do not let the change above convince you that everything is different inside a module. It is not. Compare how you got the arm handle in Phase 4 against how you get it inside the module: + +- Local script (Phases 4-5): `arm = Arm.from_robot(machine, "arm-1")`. +- Module: `arm = cast(Arm, dependencies[Arm.get_resource_name("arm-1")])`. + +The resource name, `"arm-1"`, is identical in both, and the same is true of `gripper-1`, `cam-1`, every pose switch, and `vision-segment`. Only the retrieval mechanism changes, from calling `from_robot` on a connected `machine` handle to looking the resource up in the `dependencies` mapping `reconfigure` received. `transform_pose` is the only resource access in this workshop that does not follow this pattern, for the reason described above. +{{< /alert >}} ## do_command and a scheduled job - +With dependencies wired up and `transform_pose` reachable, assemble your Phase 5 pick-and-place logic into a single method on the module, the same detection, transform, pose math, and motion calls, unchanged. What differs is how that method gets triggered. + +For the minimal viable module, trigger it through `do_command`. `do_command` is a generic handler every module exposes for commands that do not fit the typed component or service APIs. A small illustrative sketch: + +```python +async def do_command(self, command, *, timeout=None, **kwargs): + if command.get("action") == "pick_cycle": + success = await self.run_pick_cycle() + return {"success": success} + return {} +``` + +From the **CONTROL** tab, find your module's test card and send a command such as `{"action": "pick_cycle"}` to run one full pick-and-place cycle on demand, the same cycle you watched run from your script in Phase 5, now running on the robot instead of your laptop. + +{{< checkpoint >}} +Sending a `do_command` trigger runs one complete pick-and-place cycle: detection, transform, approach, grasp, travel, and place, ending with a block in the bin. This is the same sequence from Phase 5's checkpoint, now triggered from the CONTROL tab instead of a script you ran locally. +{{< /checkpoint >}} + +That manual trigger is the whole minimal viable module. Level 2 is wiring the same trigger to something other than your own hand: an internal loop that sleeps between cycles and calls `run_pick_cycle` on a cadence, or an external scheduler that sends the same `do_command` on a timer. Either approach reuses everything you already built in this phase; only the thing that calls `run_pick_cycle` changes, from a person on the CONTROL tab to a clock. + +## Where you landed + +You now have the same pick-and-place loop running two ways: as a script you control from your own laptop, and as a module that keeps running on the robot without one. Phase 5 gave you the complete win. This phase gave you the option to deploy it. There is no next phase; the workshop ends here. {{< workshop-nav >}} From 1b1515841500e796317afc08b8a7c2a89bde4ec3 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 14:24:22 -0400 Subject: [PATCH 41/44] docs(tutorials): final cross-page consistency polish Reconcile _index pre-provisioning note with the pre-provisioned camera frame calibration; describe the companion config files accurately; fix Phase 3 link title drift; note Phase 5 replaces both the approach and grasp poses; link the reference solution from the Phase 5 debugging guide. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01B26gLpfchSJJMgUg6rCFKS --- .../pick-and-place/04-control-the-robot-from-python.md | 2 +- .../pick-and-place/05-perception-guided-picking.md | 4 ++-- docs/tutorials/pick-and-place/_index.md | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md b/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md index 647415d89c..6af9cb5623 100644 --- a/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md +++ b/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md @@ -145,6 +145,6 @@ Most Phase 4 problems fall into one of a few categories: - **Resource-name mismatches.** If `Arm.from_robot(machine, "arm-1")` or a similar call raises a not-found error, the name in your script does not match the name on the **CONFIGURE** tab. Names are exact strings, not approximations, so `arm-1` and `arm_1` are different resources as far as the SDK is concerned. Open `resource_names` from the connection checkpoint above and compare it character for character against the names your script uses. - **A switch does nothing on `set_position(2)`.** This means the pose was never saved. Go back to the **CONTROL** tab and set that switch to position 1 to save the current arm position, as you did in Phase 3, then rerun the script. -With `resource_names` printing everything you expect and the static sequence running end to end from your own code, you have working proof that your connection, your named resources, and your saved poses all hold up under real code, not just button clicks. In Phase 5 you replace the fixed `grasp-pose` in this sequence with a position computed from live perception, so the arm picks whichever block the camera actually detects instead of always reaching for the same spot. +With `resource_names` printing everything you expect and the static sequence running end to end from your own code, you have working proof that your connection, your named resources, and your saved poses all hold up under real code, not just button clicks. In Phase 5 you replace the fixed `approach-pose` and `grasp-pose` in this sequence with positions computed from live perception, so the arm picks whichever block the camera actually detects instead of always reaching for the same spot. {{< workshop-nav >}} diff --git a/docs/tutorials/pick-and-place/05-perception-guided-picking.md b/docs/tutorials/pick-and-place/05-perception-guided-picking.md index 27f1b4237e..7874e6d89e 100644 --- a/docs/tutorials/pick-and-place/05-perception-guided-picking.md +++ b/docs/tutorials/pick-and-place/05-perception-guided-picking.md @@ -15,7 +15,7 @@ next: "/tutorials/pick-and-place/inline-module/" languages: ["python"] --- -In this phase you replace the fixed grasp pose from Phase 4 with live perception: the vision service detects a block, the frame system transforms its position into world space, and the motion service plans a collision-free pick. +In this phase you replace the fixed approach and grasp poses from Phase 4 with live perception: the vision service detects a block, the frame system transforms its position into world space, and the motion service plans a collision-free pick. {{< workshop-phases >}} @@ -232,7 +232,7 @@ This is a refinement, not a requirement for a working pick loop. Try the unconst ## Debugging guide -Work through these in order. The first one causes most of the rest. +Work through these in order. The first one causes most of the rest. If you get stuck, compare your loop against the complete [`reference-solution.py`](https://github.com/viam-devrel/pick-and-place/blob/main/scripts/reference-solution.py) in the companion repo. - **Did you detect from `home-pose`?** This is the first thing to check for nearly every perception symptom below. If the `await home.set_position(2)` guard is missing before a `get_object_point_clouds` call, or if you added a second detection somewhere that skips it, every downstream pose is computed against the wrong camera position. - **No objects detected.** Open the **CONTROL** tab and run the `vision-segment` card by hand while a block sits in view. If that also returns nothing, check the `shape-detector` card on its own: a detector that finds nothing means the block is out of frame, or lighting has changed enough to affect the shape detection. If `shape-detector` finds the block but `vision-segment` does not, check that a block is close enough and clearly separated from the table surface for the depth fusion step to segment it. diff --git a/docs/tutorials/pick-and-place/_index.md b/docs/tutorials/pick-and-place/_index.md index 62e14e138a..ae897ed33d 100644 --- a/docs/tutorials/pick-and-place/_index.md +++ b/docs/tutorials/pick-and-place/_index.md @@ -46,7 +46,7 @@ Phases 1 through 5 are the core workshop. Phase 6 is optional. 1. **[Platform mental model](/tutorials/pick-and-place/platform-mental-model/)** (~15 min) 2. **[Configure resources and explore the app](/tutorials/pick-and-place/configure-resources/)** (~20 min) -3. **[Static positions and safety obstacles](/tutorials/pick-and-place/static-positions/)** (~20 min) +3. **[Static positions and obstacles](/tutorials/pick-and-place/static-positions/)** (~20 min) 4. **[Control the robot from Python](/tutorials/pick-and-place/control-the-robot-from-python/)** (~15 min, milestone one) 5. **[Perception-guided picking](/tutorials/pick-and-place/perception-guided-picking/)** (~22 min, milestone two) 6. **[Inline module](/tutorials/pick-and-place/inline-module/)** (~20 min, optional) @@ -76,11 +76,11 @@ If either command fails, revisit the checklist above before continuing. - **Physical hardware ready:** start at [Phase 1](/tutorials/pick-and-place/platform-mental-model/). - **Provisioning your own hardware:** complete the hardware setup guide first (forthcoming), then return here for [Phase 1](/tutorials/pick-and-place/platform-mental-model/). -Only the physical hardware, viam-agent, and viam-server may be pre-provisioned for you. Configuring the arm, gripper, camera, and vision and motion services is always your hands-on work in this workshop, starting in Phase 2. +Only the physical hardware, viam-agent, viam-server, and the frame calibration (the camera's mounting offset on the arm) may be pre-provisioned for you or come from the hardware setup guide. Configuring the arm, gripper, camera, and the vision and motion services is always your hands-on work in this workshop, starting in Phase 2. ## Companion code All supporting files for this workshop live in the [viam-devrel/pick-and-place](https://github.com/viam-devrel/pick-and-place) repository. -- `config/` holds a machine config fragment for each phase. Use it to check your work after you configure resources by hand, not as something to import wholesale. +- `config/` holds a machine config fragment and an obstacles template. Use them to check your work after you configure resources by hand, not as something to import wholesale. - `scripts/` holds the starter script for Phase 4 and the reference solution for Phase 5. From 3de88f951b87082c0952d451b30c3ad1daee16da Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 15:52:41 -0400 Subject: [PATCH 42/44] chore: remove local planning docs from PR These design and implementation notes are workflow scratch and are kept locally, matching the untracked plans/ convention on this branch. --- ...k-and-place-self-serve-authoring-design.md | 274 ---------- ...-02-pick-and-place-self-serve-authoring.md | 490 ------------------ 2 files changed, 764 deletions(-) delete mode 100644 plans/2026-07-02-pick-and-place-self-serve-authoring-design.md delete mode 100644 plans/2026-07-02-pick-and-place-self-serve-authoring.md diff --git a/plans/2026-07-02-pick-and-place-self-serve-authoring-design.md b/plans/2026-07-02-pick-and-place-self-serve-authoring-design.md deleted file mode 100644 index 7be7181974..0000000000 --- a/plans/2026-07-02-pick-and-place-self-serve-authoring-design.md +++ /dev/null @@ -1,274 +0,0 @@ -# Design: Restructure + Author the Pick-and-Place Workshop to the Finalized Self-Serve Plan - -**Date:** 2026-07-02 -**Status:** Approved (brainstorm complete; ready for implementation planning) -**Author:** nick.hehr (with Claude Code) -**Branch:** `workshop-format-pick-and-place` -**Related:** - -- `../pick-and-place/pick-n-place-tutorial-plan.md` — the finalized content spec (source of truth for page content) -- `../pick-and-place/tutorial-review-notes.md` — per-phase self-serve decision log -- `../pick-and-place/docs/plans/2026-07-01-self-serve-tutorial-review-fixes.md` — the upstream plan that produced the finalized spec (explicitly defers "authoring the Hugo content pages" to this effort) -- `plans/2026-06-30-tutorials-featured-index-and-workshop-nav-design.md` — landing/index/sidebar work already on this branch (unchanged by this effort) - -## Context - -The multi-page pick-and-place workshop lives at `docs/tutorials/pick-and-place/` -(Hugo + Docsy). It currently has **five** phase pages, of which only Phase 3 is -substantially authored; Phases 1, 2, 4, 5 are structured stubs (headings + -`` markers). The pages were written against an **earlier, -facilitated-workshop** version of the plan. - -Meanwhile the plan has been finalized as a **self-serve** tutorial -(`pick-n-place-tutorial-plan.md`), incorporating a full review pass -(`tutorial-review-notes.md`). The upstream review-fixes plan updated the spec -and explicitly deferred authoring the Hugo pages — **that deferred authoring is -this effort.** - -The companion code repo is published at - (local working copy at -`../pick-and-place/`), containing `scripts/starter-script.py`, -`scripts/reference-solution.py`, `scripts/pyproject.toml`, -`config/machine-fragment.json`, `config/obstacles-template.json`, and -`setup/frame-calibration-worksheet.md`. All code and JSON in the authored pages -is grounded in this repo, not invented. - -## The gap (why this is more than "fill the TODOs") - -The finalized plan differs from what is on the branch in three ways: - -1. **Structural (5 → 6 phases).** Perception splits out of Phase 4 into a - dedicated Phase 5; the inline module moves to Phase 6; Phase 4 is renamed - from "Local Python script" to "Control the robot from Python" and covers the - static sequence only. -2. **Voice (facilitated → self-serve).** The already-authored `_index.md` and - Phase 3 actively contradict the finalized plan and must be rewritten, not - just extended. Examples on the branch today: - - `_index.md`: "Hardware pre-provisioned for you… if you are in a guided - workshop" — the plan wants self-serve two-milestone + prerequisites-gate - framing. - - `03-static-positions.md`: "Your workshop facilitator provides the table and - bin dimensions" and "the machine is already configured with - arm-position-saver switches… pre-loaded configuration" — the plan - overturns both (measure your own workspace; configure every resource by - hand; `machine-fragment.json` is a check-your-work reference, not an - import). -3. **Net-new content.** Every TODO section across the phases must be authored to - the plan's per-page spec, grounded in the companion code. - -## Goals - -1. Ship a six-phase, self-serve pick-and-place workshop whose content matches - `pick-n-place-tutorial-plan.md` and whose code/JSON is verified against the - published companion repo. -2. Preserve the branch's established house style and the landing/index/sidebar - work already done — the sidebar/nav shortcodes are load-bearing and must keep - working. -3. Keep the build green at every commit. - -## Non-goals - -- No changes to the landing page, `/tutorials/all/` archive, `tutorial-card`, - `tutorial-hero`, or `sidebar.html` work from the 2026-06-30 design. -- No new shortcodes. Author with the existing - `workshop-phases` / `workshop-nav` / `checkpoint` / `alert` shortcodes. The - plan's speculative `code-file` shortcode and `data/tutorials.yaml` stay - deferred; code is shown as inline fenced blocks with links to the companion - repo. -- No companion-repo changes and no separate hardware-setup guide in this effort - (the plan references both; they are downstream). The `_index` links to the - setup guide as "forthcoming." -- No edits to the facilitated workshop slides. - -## House style (authoring must follow the branch, not the plan's speculative schema) - -The finalized plan sketches a speculative frontmatter/shortcode schema -(`tutorial:`, `difficulty:`, `checkpoint.html`, `content/`). **Ignore it** in -favor of the conventions the branch actually uses: - -- **Directory:** `docs/tutorials/pick-and-place/` (this repo uses `docs/`, not - `content/`). -- **Frontmatter keys:** `title`, `linkTitle` (numbered, e.g. `"4. Control from - Python"`), `type: "docs"`, `slug`, `weight`, `description`, `workshop: - "pick-and-place"`, `toc_hide: true`, `phase`, `phase_total`, `time_estimate`, - `prev`, `next`, `languages`. -- **Shortcodes:** `{{< workshop-phases >}}` (top of body), `{{< checkpoint >}}`, - `{{< alert title="…" color="note" >}}`, `{{< workshop-nav >}}` (bottom). -- **Companion links:** real `https://github.com/viam-devrel/pick-and-place` - URLs. -- **Prose rules:** enforced by prettier, markdownlint (`.markdownlint.yaml`), - and vale (no em dashes, sentence-case headings, no parenthetical plurals, - etc.). - -## Design - -### Part 1 — Restructure (single prep commit; mechanical; build stays green) - -| Action | File | Key frontmatter | -| --- | --- | --- | -| Keep | `01-platform-mental-model.md` | `phase: 1`, `phase_total: 6`, `weight: 10`, slug `platform-mental-model` | -| Keep | `02-configure-resources.md` | `phase: 2`, `phase_total: 6`, `weight: 20`, slug `configure-resources` | -| Keep | `03-static-positions.md` | `phase: 3`, `phase_total: 6`, `weight: 30`, slug `static-positions` | -| Rename + strip perception | `04-local-python-script.md` → `04-control-the-robot-from-python.md` | title "Phase 4: Control the robot from Python", slug `control-the-robot-from-python`, `phase: 4`, `phase_total: 6`, `weight: 40` | -| New file | `05-perception-guided-picking.md` | title "Phase 5: Perception-guided picking", slug `perception-guided-picking`, `phase: 5`, `phase_total: 6`, `weight: 50` | -| Renumber | `05-inline-module.md` → `06-inline-module.md` | title "Phase 6: Inline module", slug `inline-module`, `phase: 6`, `phase_total: 6`, `weight: 60` | - -Also in this commit: - -- Bump `phase_total: 6` on every phase page. -- Rewire the `prev`/`next` chain to: - `platform-mental-model` → `configure-resources` → `static-positions` → - `control-the-robot-from-python` → `perception-guided-picking` → - `inline-module`. -- Rebuild the `_index.md` phase list to six entries with the plan's time - estimates (15 / 20 / 20 / 15 / 22 / 20 min) and corrected links/titles. -- Move the perception-related TODO scaffolding out of Phase 4 and into the new - Phase 5 stub so Phase 4 covers the static sequence only. - -The `workshop-phases` / `workshop-nav` shortcodes derive phase order from -`section.RegularPages.ByWeight`, so the new Phase 5 page auto-slots once its -`weight` is set. No aliases/redirects are required: the branch is not merged or -live, so no public URLs change. The Phase 4 slug change -(`local-python-script` → `control-the-robot-from-python`) is therefore safe. - -### Part 2 — Per-page content (one authoring commit each, grounded in companion code) - -Content requirements are the per-page spec in `pick-n-place-tutorial-plan.md`; -the self-serve rationale is in `tutorial-review-notes.md`. Summary per page: - -- **`_index.md` (rewrite, facilitated → self-serve).** Two-milestone framing - (Phase 4 = milestone one, "drive the robot from your own code," a bankable - win; Phase 5 = milestone two, perception; Phase 6 optional; **Phases 1–5 - core**). Prerequisites **gate** with verify commands *and* install links - (Python 3.10+, `viam-sdk`, a working terminal, a Viam account with a Live - machine). Login/machine-access as a prerequisite. Environment validation - (`uv` recommended, can `import viam`) before Phase 4. Hardware context via the - setup-guide link + header image, not a tour. Two entry paths distinguishing - hardware provisioning (may be pre-provisioned) from resource configuration - (always hands-on). Link to companion repo. Remove resolved TODOs. -- **P1 `01-platform-mental-model.md`.** Three-layer architecture, SDK - connection, config-as-source-of-truth, components vs. services, dependency - graph. Live "open your CONFIGURE tab, find `arm-1`, read its - `namespace:family:model`" grounding throughout (overrides the old "no live - interactions yet"). Keep `shape-detector` / `vision-segment` foreshadow in the - dependency graph. Builtin (RDK) vs. module-provided resources, with the - module-download moment previewed (it lands in P2). Closing self-check. -- **P2 `02-configure-resources.md`.** Hands-on config of `arm-1`, `gripper-1`, - `cam-1` (resource table as target state, not "what's pre-configured"). - Configuring `viam:ufactory:xArm6` is the module-download moment. 3D-scene - active task: "jog joint 1, watch `cam-1` move with the arm" (sets up the - wrist-camera rule). Gripper `IsHoldingSomething` active task. Per-card - checkpoints (camera, arm, gripper). Vision pipeline is **not** configured here - — it moves to P5. -- **P3 `03-static-positions.md` (self-serve rewrite).** Problem-isolation - rationale kept as proof of value (real production workflow). The five key - poses. Explicit hands-on arm-position-saver setup (`erh:vmodutils` from - Registry, one switch per pose, `arm: arm-1`), using the app's "duplicate" - feature for poses 2–5; remove "already configured / pre-loaded" framing. - Teach frame-geometry config and have the learner **measure their own - workspace** (remove "facilitator provides dimensions"). Safety walls framed - as a production-motion feature. SetPosition `1`=save / `2`=execute callout + - "does nothing" troubleshooting aside. `machine-fragment.json` / - `obstacles-template.json` as check-your-work references. Reconcile the - obstacle JSON against the real `config/obstacles-template.json` and the Viam - motion-service schema (resolves the existing "illustrative JSON" TODO). -- **P4 `04-control-the-robot-from-python.md`.** Why script before module - (comparison). Sell programmability + the Control-tab-UI→SDK-method-name - mapping. Clone the companion `scripts/` project and `uv run starter-script.py` - (`uv` primary, pip fallback; env already validated in the prerequisites gate). - Reference the Connect-tab boilerplate rather than authoring the connection - from scratch. Secrets-handling note. Obstacles live in machine config and - apply to every `motion.move` automatically — **not** passed in code. - Checkpoints: `resource_names` prints all resources; static sequence runs - end-to-end from Python. **Static sequence only — no perception.** -- **P5 `05-perception-guided-picking.md` (new).** Configure the vision pipeline - here (shape-detector → `vision-segment`, `detections-to-segments`) + - Control-tab test. Frame system + `transform_pose`. **Home-pose guard clause** - in the perception loop (assert/return to `home-pose` before every detect; - structurally enforced; first entry in the debugging guide). Worked - approach-offset as a fully worked example; learner practices the - gripper-TCP grasp offset (productive struggle). Explicit - `motion.move("gripper-1", …)` frame semantics (drives the gripper frame to the - world pose — contrast with the arm/UI MoveToPosition). Perception API using - `len(o.point_cloud)`. Symptom → 3D-scene-tab debugging with a back-link to P3 - obstacle/safety-wall config. Granular sub-checkpoints (detector works → - transform yields sane world coords → approach reachable → grasp succeeds). All - code verified against `reference-solution.py`, including the gripper-TCP - offset value and `PoseInFrame` kwargs. -- **P6 `06-inline-module.md` (optional).** Framed as an optional next step (no - time pressure). Strong "why bother" (survives disconnection, auto-restart, OTA - deploy, scheduled runs). Honest "mostly packaging + one real change." Tiered - scope: MVP (repackage + `do_command`, manual trigger) vs. level-2 (scheduled - job / autonomous). `validate_config` + `reconfigure` (dependency injection). - **Corrected `transform_pose`-in-module pattern:** a single reused in-module - `RobotClient` authenticated from `VIAM_API_KEY` / `VIAM_API_KEY_ID` / - `VIAM_MACHINE_FQDN` env vars — there is **no** `FrameSystemClient` and no - injected frame-system dependency (this reverses the current stub's incorrect - note). `from_robot` ↔ `cast + get_resource_name` bridge callout. ~1 min cloud - build time stated upfront. - -### Part 3 — Correctness grounding - -- Python snippets are drawn from / verified against - `../pick-and-place/scripts/{starter-script.py,reference-solution.py}`. -- Machine-config and obstacle JSON are verified against - `../pick-and-place/config/{machine-fragment.json,obstacles-template.json}` and - the Viam motion-service obstacle schema. -- Two known correctness items to resolve during authoring: the "illustrative" - obstacle JSON in P3, and the fabricated `FrameSystemClient` note in P6. -- Remove now-stale TODO comments that say the companion repo "does not exist yet" - (it is published). - -### Part 4 — Verification - -Per commit, run the CLAUDE.md pre-PR checks on the changed Markdown, in order: - -1. `npx prettier --write docs/tutorials/pick-and-place/**/*.md` -2. `npx markdownlint-cli --config .markdownlint.yaml docs/tutorials/pick-and-place/**/*.md` -3. `vale sync && vale docs/tutorials/pick-and-place/` -4. `make build-prod` (must complete without errors) - -Spot-check with `hugo server`: the six phases appear in order in the -`workshop-phases` box and the sidebar with the current phase highlighted; -`workshop-nav` prev/next chains correctly across all six; the landing card and -`/tutorials/all/` archive are unaffected. No unit tests (docs site). - -### Part 5 — Sequencing - -Commit this design doc first, then: - -1. Restructure prep commit (Part 1). -2. `_index.md` rewrite. -3. Phase 1 authoring. -4. Phase 2 authoring. -5. Phase 3 self-serve rewrite. -6. Phase 4 authoring. -7. Phase 5 authoring (new page). -8. Phase 6 authoring (corrected module API). - -Each step keeps the build green and passes the four checks before committing. - -## Risks / watch-items - -- **Nav derivation:** confirm the new Phase 5 page slots correctly in - `workshop-phases` / `workshop-nav` (weight-ordered) and that `phase_total: 6` - renders consistently. -- **Slug change:** the Phase 4 slug change must be reflected in every `prev`/`next` - reference and the `_index` phase list; grep for the old - `/local-python-script/` and `/inline-module/` (position 5) URLs after the - restructure. -- **Obstacle JSON schema:** the existing P3 JSON is flagged illustrative; - reconcile against the real template + Viam schema before publishing. -- **Module API correctness:** ensure the P6 rewrite fully removes the - `FrameSystemClient` pattern and matches the in-module `RobotClient` reference. -- **Scope creep into landing/sidebar:** this effort touches only - `docs/tutorials/pick-and-place/*`; the 2026-06-30 layout work is out of scope. - -## Out-of-scope follow-ups (noted, not now) - -- Companion-repo code changes (e.g. adding the home-pose guard clause to - `reference-solution.py`). -- The separate hardware-setup how-to guide - (`docs/guides/hardware-setup/xarm6-pick-and-place.md`). -- The `code-file` shortcode and `data/tutorials.yaml` card-grid data file. -- Header/hardware-overview imagery for `_index` and the landing card. diff --git a/plans/2026-07-02-pick-and-place-self-serve-authoring.md b/plans/2026-07-02-pick-and-place-self-serve-authoring.md deleted file mode 100644 index a11e0b4804..0000000000 --- a/plans/2026-07-02-pick-and-place-self-serve-authoring.md +++ /dev/null @@ -1,490 +0,0 @@ -# Pick-and-Place Self-Serve Workshop Authoring — Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Restructure the pick-and-place workshop from 5 to 6 phases and author/reconcile every page to the finalized self-serve spec, with all code and JSON verified against the published companion repo. - -**Architecture:** One mechanical restructure commit establishes the 6-phase skeleton (renames, split, renumber, nav chains) with the build green. Then one authoring commit per page (`_index`, Phases 1–6), each grounded in the finalized content spec and the real companion code, each passing the four pre-PR checks. A final consistency sweep closes it out. - -**Tech Stack:** Hugo + Docsy static site (Markdown under `docs/tutorials/pick-and-place/`), project shortcodes (`workshop-phases`, `workshop-nav`, `checkpoint`, `alert`), prettier / markdownlint / vale / `make build-prod`. - -**Sources of truth:** - -- Content spec (per-page requirements): `../pick-and-place/pick-n-place-tutorial-plan.md` -- Self-serve rationale / decisions: `../pick-and-place/tutorial-review-notes.md` -- Verified code: `../pick-and-place/scripts/{starter-script.py,reference-solution.py,pyproject.toml,.python-version}` -- Verified config: `../pick-and-place/config/{machine-fragment.json,obstacles-template.json}` -- Design: `plans/2026-07-02-pick-and-place-self-serve-authoring-design.md` - -**Key facts locked from the companion code (do not drift from these):** - -- Detection is **shape-based**: vision service `vision-segment` (model `detections-to-segments`), fed by a `shape-detector`. Objects come from `vision.get_object_point_clouds("cam-1")`; the label is `obj.geometries.geometries[0].label`. -- There are **five poses**, one bin: `home-pose`, `approach-pose`, `grasp-pose`, `travel-pose`, `place-pose`. **Not** per-color bins. -- Poses are saved with `erh:vmodutils` arm-position-saver **switch** components; `set_position(2)` executes a saved pose, `set_position(1)` saves, `set_position(0)` clears. -- Obstacles are **`erh:vmodutils:obstacle` components** (`api: rdk:component:gripper`) added to the machine `components` array — **not** a motion-service WorldState array. They appear in `resource_names` as grippers. -- Motion service name is `"builtin"`. `motion.move(component_name="gripper-1", destination=PoseInFrame(reference_frame="world", pose=...))` drives the **gripper** frame to a world pose. -- Constants: `GRIPPER_LENGTH_MM = 60` (grasp offset), `APPROACH_MM = 100`, settle `0.3 s`. Optional straight-down descent uses `Constraints(linear_constraint=[LinearConstraint(line_tolerance_mm=5.0)])`. -- Python: `requires-python = ">=3.10"`, repo pins `3.11` via `.python-version`. `uv` is primary; pip is fallback. -- Companion repo is **published** at ; remove any "does not exist yet" TODO. - -**House style (follow the branch, not the plan's speculative schema):** frontmatter keys `title`, `linkTitle`, `type: "docs"`, `slug`, `weight`, `description`, `workshop: "pick-and-place"`, `toc_hide: true`, `phase`, `phase_total`, `time_estimate`, `prev`, `next`, `languages`. Body opens with `{{< workshop-phases >}}` and ends with `{{< workshop-nav >}}`. Callouts use `{{< checkpoint >}}` and `{{< alert title="…" color="note" >}}`. Companion links use real `https://github.com/viam-devrel/pick-and-place` URLs. - -**Per-task verification convention:** after editing, run the four checks scoped to the workshop directory, in order (prettier → markdownlint → vale → build). Commit only when all pass. Full commands are in Task 9; abbreviated per task as "run the four checks." - ---- - -### Task 1: Restructure to six phases (mechanical prep commit) - -**Goal:** Establish the 6-phase file/frontmatter/nav skeleton with the build green. No prose authoring yet beyond moving existing stub content. - -**Files:** - -- Rename: `docs/tutorials/pick-and-place/04-local-python-script.md` → `04-control-the-robot-from-python.md` -- Create: `docs/tutorials/pick-and-place/05-perception-guided-picking.md` -- Rename: `docs/tutorials/pick-and-place/05-inline-module.md` → `06-inline-module.md` -- Modify: `01-platform-mental-model.md`, `02-configure-resources.md`, `03-static-positions.md`, `_index.md` - -**Step 1: Rename files with git mv (preserve history)** - -```bash -cd /Users/nick.hehr/src/viam-docs/docs/tutorials/pick-and-place -git mv 04-local-python-script.md 04-control-the-robot-from-python.md -git mv 05-inline-module.md 06-inline-module.md -``` - -**Step 2: Set `phase_total: 6` on all six phase pages** - -In each of `01`…`06`, change `phase_total: 5` to `phase_total: 6`. - -**Step 3: Fix Phase 4 frontmatter (renamed file)** - -In `04-control-the-robot-from-python.md`, set: - -```yaml -title: "Phase 4: Control the robot from Python" -linkTitle: "4. Control from Python" -slug: "control-the-robot-from-python" -weight: 40 -phase: 4 -phase_total: 6 -time_estimate: "15 minutes" -prev: "/tutorials/pick-and-place/static-positions/" -next: "/tutorials/pick-and-place/perception-guided-picking/" -description: "Connect from your laptop and drive the saved static pick-and-place sequence from a Python script." -``` - -Then **remove the perception sections** from the Phase 4 body (the "The frame system and transforms", "Add perception", and "Pass obstacles to the motion service" TODO blocks) — they move to Phase 5 in Step 5. Leave the static-sequence and connection scaffolding. - -**Step 4: Fix Phase 6 frontmatter (renamed file)** - -In `06-inline-module.md`, set: - -```yaml -title: "Phase 6: Inline module" -linkTitle: "6. Inline module" -weight: 60 -phase: 6 -phase_total: 6 -time_estimate: "20 minutes" -prev: "/tutorials/pick-and-place/perception-guided-picking/" -``` - -(Keep `slug: "inline-module"`.) - -**Step 5: Create the Phase 5 stub** - -Create `05-perception-guided-picking.md` with frontmatter and a section skeleton (prose authored in Task 7). Move the perception TODOs removed from Phase 4 here. - -```yaml ---- -title: "Phase 5: Perception-guided picking" -linkTitle: "5. Perception-guided picking" -type: "docs" -slug: "perception-guided-picking" -weight: 50 -description: "Add the vision pipeline and write the perception loop that detects a block, transforms it to the world frame, and picks it with motion planning." -workshop: "pick-and-place" -toc_hide: true -phase: 5 -phase_total: 6 -time_estimate: "22 minutes" -prev: "/tutorials/pick-and-place/control-the-robot-from-python/" -next: "/tutorials/pick-and-place/inline-module/" -languages: ["python"] ---- -``` - -Body: `{{< workshop-phases >}}` at top, `{{< workshop-nav >}}` at bottom, with `## Configure the vision pipeline`, `## The frame system and transform_pose`, `## Detect from home (the wrist-camera rule)`, `## Compute the approach and grasp poses`, `## Run the full pick loop`, `## Debugging guide` section stubs. - -**Step 6: Update the remaining prev/next links for the renumber** - -In `03-static-positions.md`, set `next: "/tutorials/pick-and-place/control-the-robot-from-python/"`. -Grep to confirm no stale slugs remain (old `/local-python-script/`; and `/inline-module/` must now only appear as Phase 5's `next` and Phase 6's own slug): - -```bash -cd /Users/nick.hehr/src/viam-docs -grep -rn "local-python-script" docs/tutorials/pick-and-place/ # expect: zero -grep -rn "phase_total: 5" docs/tutorials/pick-and-place/ # expect: zero -``` - -**Step 7: Update the `_index.md` phase list to six entries (interim)** - -In `_index.md`, replace the five-item Phases list (lines ~41-47) and the "structured as five sequential phases" sentence with six entries and corrected links/titles/estimates. (The full self-serve rewrite of surrounding prose is Task 2; here just keep links valid so the build passes.) - -```markdown -## Phases - -1. **[Platform mental model](/tutorials/pick-and-place/platform-mental-model/)** (~15 min) -2. **[Configure resources and explore the app](/tutorials/pick-and-place/configure-resources/)** (~20 min) -3. **[Static positions and safety obstacles](/tutorials/pick-and-place/static-positions/)** (~20 min) -4. **[Control the robot from Python](/tutorials/pick-and-place/control-the-robot-from-python/)** (~15 min) — milestone one -5. **[Perception-guided picking](/tutorials/pick-and-place/perception-guided-picking/)** (~22 min) — milestone two -6. **[Inline module](/tutorials/pick-and-place/inline-module/)** (~20 min, optional) -``` - -**Step 8: Run the four checks** - -Run prettier → markdownlint → vale → `make build-prod` (see Task 9 for exact commands). Expected: build completes without errors; `workshop-phases`/`workshop-nav` render six phases. - -**Step 9: Commit** - -```bash -git add docs/tutorials/pick-and-place/ -git commit -m "refactor(tutorials): restructure pick-and-place to six phases - -Split perception into Phase 5, rename Phase 4 to control-the-robot-from-python, -renumber inline module to Phase 6, bump phase_total, rewire prev/next." -``` - ---- - -### Task 2: Rewrite `_index.md` (facilitated → self-serve) - -**Goal:** Carry the orientation a facilitator would deliver live: two-milestone framing, a prerequisites gate, self-serve entry paths. - -**Files:** Modify `docs/tutorials/pick-and-place/_index.md` - -**Spec:** `pick-n-place-tutorial-plan.md` → "`pick-and-place/_index.md`" bullets, and `tutorial-review-notes.md` → Phase 0. - -**Content requirements:** - -- **Intro:** six phases; "Phases 1–5 are the core workshop; Phase 6 is optional." Correct the detection narrative to **shape-based sorting into a bin** (not "sorted by color"). Two-milestone framing: Phase 4 (drive the robot from your own code) = milestone one, a bankable win; Phase 5 (perception) = milestone two. -- **What you'll build:** one paragraph — xArm6 + finger gripper + wrist-mounted RealSense; a shape-detection vision service finds blocks, the motion service plans collision-free picks, blocks are placed in the bin; by end of Phase 5 a Python script runs the full detect-pick-place loop. -- **Hardware:** keep the existing four-item list. -- **Phases:** the six-item list from Task 1 Step 7, with the milestone annotations. -- **Prerequisites gate** (replace the current "Hardware pre-provisioned for you / guided workshop" block): - - A checklist with verification commands **and** install links: Python 3.10+ (link to python.org / uv install), the Viam Python SDK (`uv add viam-sdk`; link to SDK docs), a working terminal, and a Viam account with an accessible machine (link to app.viam.com). - - **Login/machine-access as a prerequisite:** "log in at app.viam.com, open your machine, confirm the green **Live** indicator." - - **Environment validation** before Phase 4: a working Python env (`uv` recommended) that can `import viam`: - - ```sh - python3 --version # 3.10 or newer - uv run python -c "import viam; print(viam.__version__)" # prints a version - ``` - - - **Two entry paths**, distinguishing hardware provisioning from resource configuration: "Physical hardware ready → start at Phase 1" / "Provisioning your own hardware → complete the setup guide first (forthcoming)." Note that only physical hardware + viam-agent/server may be pre-provisioned; **resource configuration is always the learner's hands-on work.** -- **Companion code:** keep the `viam-devrel/pick-and-place` link; describe `config/` (check-your-work reference), `scripts/` (starter + reference). **Remove** the "companion repo does not exist yet" TODO. - -**Verification:** run the four checks. `grep -n "pre-provisioned by instructor\|sorted by color\|does not exist yet" docs/tutorials/pick-and-place/_index.md` → expect zero. - -**Commit:** `docs(tutorials): rewrite pick-and-place overview for self-serve (milestones, prerequisites gate)` - ---- - -### Task 3: Author Phase 1 — `01-platform-mental-model.md` - -**Goal:** Concept phase grounded in live app interaction, ending with a self-check. - -**Files:** Modify `docs/tutorials/pick-and-place/01-platform-mental-model.md` - -**Spec:** `pick-n-place-tutorial-plan.md` → "`01-platform-mental-model.md`"; `tutorial-review-notes.md` → Phase 1. - -**Content requirements (replace TODO stubs with prose):** - -- Three questions up top the learner should be able to answer by the end (state them; used by the closing self-check). -- Three-layer architecture (cloud app / `viam-agent` / `viam-server`), SDK connection, config-as-source-of-truth, resource model (components vs. services), the dependency graph. -- **Live grounding** in each section: "open your **CONFIGURE** tab, find `arm-1`, read its `namespace:family:model`," etc. — overrides any "no live interactions yet" stance. -- Keep the perception-pipeline **foreshadow**: use `shape-detector` and `vision-segment` as concrete examples of services / composing resources (they build these in Phase 5). -- **Builtin (RDK) vs. module-provided resources:** most added functionality comes from modules; explain how modules interact with `viam-server`, and preview the module-download moment (it lands in Phase 2 when they add the xArm). -- **Closing self-check** ({{< alert … >}} or plain): "you should now be able to answer the three questions from the top — if not, re-skim." - -**Verification:** run the four checks. - -**Commit:** `docs(tutorials): author Phase 1 platform mental model (self-serve)` - ---- - -### Task 4: Author Phase 2 — `02-configure-resources.md` - -**Goal:** First hands-on phase: configure every resource by hand and verify with test cards. - -**Files:** Modify `docs/tutorials/pick-and-place/02-configure-resources.md` - -**Spec:** `pick-n-place-tutorial-plan.md` → "`02-configure-resources.md`"; `tutorial-review-notes.md` → Phase 2 and the cross-cutting "resources are hands-on" correction. - -**Content requirements:** - -- Learner configures **each** hardware resource by hand: `arm-1` (`viam:ufactory:xArm6`), `gripper-1`, `cam-1`. Present the resource table as **target state**, not "what's pre-configured." -- Configuring the xArm is the **module-download moment**: add the arm, watch `viam-server` download + start the module live (delivers the Phase 1 builtin-vs-module lesson). -- **CONTROL tab test cards** with per-card **checkpoints**: camera card (see a frame), arm card (jog joints), gripper card. -- **3D scene tab active task:** "jog joint 1 and watch the `cam-1` frame move with the arm" — this is the wrist-mounted-camera insight, load-bearing for Phase 5's detect-from-home rule. -- **Gripper `IsHoldingSomething` task:** place a block between the fingers, press **Grab**, observe the status; add a gripper checkpoint for symmetry. -- **The vision pipeline is NOT configured here** — it moves to Phase 5. Remove any vision-service config from this page if present. - -**Verification:** run the four checks. `grep -n "pre-configured\|vision-segment\|shape-detector" docs/tutorials/pick-and-place/02-configure-resources.md` → expect zero (vision belongs to Phase 5). - -**Commit:** `docs(tutorials): author Phase 2 hands-on resource configuration` - ---- - -### Task 5: Rewrite Phase 3 — `03-static-positions.md` (self-serve + model correction) - -**Goal:** Self-serve rewrite AND correct the detection/obstacle model: five poses with a single `place-pose`, obstacles as `erh:vmodutils:obstacle` components, measure-your-own-workspace. - -**Files:** Modify `docs/tutorials/pick-and-place/03-static-positions.md` - -**Spec:** `pick-n-place-tutorial-plan.md` → "`03-static-positions.md`"; `tutorial-review-notes.md` → Phase 3. Verify JSON against `../pick-and-place/config/obstacles-template.json` and pose names against `reference-solution.py`. - -**Content corrections from the current page (all required):** - -1. **Five poses, single bin.** Replace the per-color bin poses (`red-bin-pose`, `blue-bin-pose`, `green-bin-pose`) with the five canonical poses: `home-pose`, `approach-pose`, `grasp-pose`, `travel-pose`, `place-pose`. Update the two tables accordingly. -2. **Hands-on pose setup** (remove "the machine is already configured… pre-loaded configuration"): add `erh:vmodutils` arm-position-saver from the Registry, add one **switch** per pose with `arm: arm-1`; configure `home-pose` fully, then use the app's **"duplicate" resource feature** for the other four. `machine-fragment.json` is the **check-your-work reference**, not an import. -3. **Measure your own workspace** (remove "Your workshop facilitator provides the table and bin dimensions"): teach how frame geometries are configured; the learner measures the table and obstacles with `GetEndPosition` and translates measurements into geometry config. -4. **Obstacles are components, not a WorldState array.** Replace the current `{"obstacles": [...]}` JSON with the real `erh:vmodutils:obstacle` component form. Use this verbatim (from `obstacles-template.json`), noting `REPLACE_WITH_MEASURED_*` placeholders and that these appear in `resource_names` as grippers: - - ```json - { - "name": "table", - "api": "rdk:component:gripper", - "model": "erh:vmodutils:obstacle", - "attributes": { - "geometries": [ - { - "label": "table", - "type": "box", - "x": 1200, - "y": 800, - "z": 30, - "translation": { "x": 0, "y": 0, "z": -15 }, - "parent": "world" - } - ] - } - } - ``` - - Explain: box `z` translation is the box **center** (half its height); the table sits below `world` z=0 so its z is negative half-thickness (`-15` for a 30 mm top). Then the two **safety walls** (`safety-wall-front`, `safety-wall-side`) as thin vertical boxes at the workspace boundary with `REPLACE_WITH_MEASURED_*` positions. -5. **Safety walls as a production-motion feature** — configured to fit the learner's workspace; pitch virtual walls as a demo of the motion planner honoring obstacles for real-world/production deployments, not just classroom safety. -6. **Keep the problem-isolation rationale** as proof of value (pose-to-pose motion is a real production workcell workflow). -7. **Keep** the SetPosition `1`=save / `2`=execute callout, plus the "SetPosition(2) does nothing → you didn't save first" troubleshooting aside. -8. Link `obstacles-template.json` and `machine-fragment.json` in the companion repo as check-your-work references; remove the "illustrative JSON must be reconciled" TODO once the JSON matches the template. - -**Verification:** run the four checks. `grep -n "facilitator provides\|red-bin\|blue-bin\|green-bin\|\"obstacles\"" docs/tutorials/pick-and-place/03-static-positions.md` → expect zero. `grep -n "erh:vmodutils:obstacle\|place-pose" …/03-static-positions.md` → expect hits. - -**Commit:** `docs(tutorials): rewrite Phase 3 for self-serve (five poses, component obstacles, measured workspace)` - ---- - -### Task 6: Author Phase 4 — `04-control-the-robot-from-python.md` - -**Goal:** Drive the **static** sequence from Python. No perception. - -**Files:** Modify `docs/tutorials/pick-and-place/04-control-the-robot-from-python.md` - -**Spec:** `pick-n-place-tutorial-plan.md` → "`04-control-the-robot-from-python.md`"; `tutorial-review-notes.md` → Phase 4. Code from `../pick-and-place/scripts/starter-script.py`. - -**Content requirements:** - -- **Why a script before a module** (comparison): programmability (loops/branches/logic) AND the Control-tab-UI-controls → SDK-method-name mapping (the cards map to methods). Make the payoff felt. -- **Get the companion project:** clone/download `viam-devrel/pick-and-place`, work in `scripts/`. `uv` is primary (`uv run python starter-script.py`; it reads `pyproject.toml`/`.python-version`); pip is fallback. Env was already validated in the prerequisites gate — this phase is connect + run. -- **Connection:** reference the **Connect tab → Python SDK** boilerplate; the starter's `connect()` mirrors it. Show the connection block and the `MACHINE_ADDRESS`/`API_KEY`/`API_KEY_ID` fill-ins. **Secrets note:** don't commit API keys; use the repo `.gitignore` or env vars. -- **Verify the connection** with `print(machine.resource_names)` — a **checkpoint**: you should see `arm-1`, `gripper-1`, `cam-1`, the poses as switches, and the obstacles as grippers. -- **Run the static sequence** (verbatim, matches `starter-script.py` TODO 4): - - ```python - await home.set_position(2) - await approach.set_position(2) - await gripper.open() - await grasp.set_position(2) - await gripper.grab() - await asyncio.sleep(0.3) # finger gripper settle - await travel.set_position(2) - await place_pose.set_position(2) - await gripper.open() - await home.set_position(2) - ``` - -- **Obstacles are not passed in code** — they live in the machine config (Phase 3) and apply to every `motion.move` automatically. No runtime WorldState. -- **Checkpoints:** `resource_names` prints all resources; the static sequence runs end-to-end from Python. -- Connection-debugging aside for common failures. - -**Verification:** run the four checks. `grep -n "get_object_point_clouds\|transform_pose\|vision" …/04-control-the-robot-from-python.md` → expect zero (perception is Phase 5). - -**Commit:** `docs(tutorials): author Phase 4 driving the static sequence from Python` - ---- - -### Task 7: Author Phase 5 — `05-perception-guided-picking.md` (new page) - -**Goal:** The hardest phase: configure vision, transform to world, compute offsets, run the pick loop. Code verified against `reference-solution.py`. - -**Files:** Modify `docs/tutorials/pick-and-place/05-perception-guided-picking.md` - -**Spec:** `pick-n-place-tutorial-plan.md` → "`05-perception-guided-picking.md`"; `tutorial-review-notes.md` → Phase 5. - -**Content requirements:** - -- **Configure the vision pipeline here** (moved from Phase 2): a `shape-detector` feeding `vision-segment` (model `detections-to-segments`); test it in the **CONTROL** tab. First sub-checkpoint: detector works in the app. -- **Frame system + `transform_pose`:** the detection is in the `cam-1` frame; the planner needs `world`. Show: - - ```python - obj_in_cam = PoseInFrame(reference_frame=CAMERA_NAME, pose=geometry.center) - obj_in_world = await machine.transform_pose(obj_in_cam, "world") - ``` - -- **Detect from home (wrist-camera rule):** the camera is wrist-mounted, so its frame moves with the arm — you MUST detect from `home-pose`. **Home-pose guard clause** structurally enforced (`await home.set_position(2)` before every detect) and made the **first** entry in the debugging guide. -- **Detection** (verbatim from reference): - - ```python - objects = await vision.get_object_point_clouds(CAMERA_NAME) - if not objects: - print("No objects detected") - return False - obj = max(objects, key=lambda o: len(o.point_cloud)) # len(), not .size - geometry = obj.geometries.geometries[0] - label = geometry.label - ``` - -- **Approach offset worked; learner practices the grasp offset.** Walk through the approach pose fully: `approach_pose = offset_pose(obj_in_world.pose, APPROACH_MM)` (`APPROACH_MM = 100`). Then have the learner compute the grasp offset themselves; the answer is `grasp_pose = offset_pose(obj_in_world.pose, GRIPPER_LENGTH_MM)` (`GRIPPER_LENGTH_MM = 60`, the gripper-TCP-to-fingertip depth). Include the `offset_pose` helper. -- **`motion.move("gripper-1", …)` semantics explicit:** it drives the **gripper** coordinate frame to the destination world pose — NOT the arm end (which is what the UI MoveToPosition / the arm component `move_to_position` do). Contrast the two so the offset math makes sense. -- **Full pick loop** (hybrid: `motion.move` for the Cartesian pick, saved switches for the place), verbatim: - - ```python - await motion.move( - component_name=GRIPPER_NAME, - destination=PoseInFrame(reference_frame="world", pose=approach_pose), - ) - await gripper.open() - await motion.move( - component_name=GRIPPER_NAME, - destination=PoseInFrame(reference_frame="world", pose=grasp_pose), - ) - await gripper.grab() - await asyncio.sleep(0.3) - await travel.set_position(2) - await place_pose.set_position(2) - await gripper.open() - await home.set_position(2) - ``` - - Mention the optional straight-down descent follow-up: `Constraints(linear_constraint=[LinearConstraint(line_tolerance_mm=5.0)])` on the grasp move. -- **Debugging guide:** symptom → **3D scene tab** (what to look for), with a back-link to Phase 3 obstacle/safety-wall config (skipping it bites here). Home-pose guard is entry #1. -- **Granular sub-checkpoints:** detector works → transform yields sane world coords → approach reachable → grasp succeeds → full loop completes. - -**Verification:** run the four checks. `grep -n "point_cloud.size\|red-bin\|move_to_position(" …/05-perception-guided-picking.md` → expect zero (use `len(...)`, single bin, gripper-frame `motion.move`). `grep -n "get_object_point_clouds\|transform_pose\|GRIPPER_LENGTH_MM" …` → expect hits. - -**Commit:** `docs(tutorials): author Phase 5 perception-guided picking` - ---- - -### Task 8: Author Phase 6 — `06-inline-module.md` (optional; corrected API) - -**Goal:** Package the script as an inline module, with the **corrected** in-module `RobotClient` pattern. - -**Files:** Modify `docs/tutorials/pick-and-place/06-inline-module.md` - -**Spec:** `pick-n-place-tutorial-plan.md` → "`06-inline-module.md`"; `tutorial-review-notes.md` → Phase 6. - -**Content requirements:** - -- **Framed as optional** (no time pressure). **Strong "why bother":** you'd want this when it must survive disconnection, auto-restart, OTA deploy, or run on a schedule. -- **Honest framing:** "mostly packaging + one real change" — the `transform_pose` access genuinely changes; don't let a "same logic, different entry point" line set a trap. -- **Tier the scope:** MVP (repackage + `do_command`, trigger manually) is the core optional path; scheduled jobs + autonomous operation are an explicit "level 2." -- Inline module editor walkthrough; `validate_config` + `reconfigure` (dependency injection). -- **CORRECTED `transform_pose` inside a module** — there is **no** `FrameSystemClient` and no injected frame-system dependency. Create a **single, reused `RobotClient`** from env vars. Verbatim: - - ```python - import os - from viam.robot.client import RobotClient - - async def create_robot_client_from_module(): - opts = RobotClient.Options.with_api_key( - api_key=os.environ["VIAM_API_KEY"], - api_key_id=os.environ["VIAM_API_KEY_ID"], - ) - return await RobotClient.at_address(os.environ["VIAM_MACHINE_FQDN"], opts) - - # self.robot_client initialized to None; create once, reuse: - if not self.robot_client: - self.robot_client = await create_robot_client_from_module() - world_pose = await self.robot_client.transform_pose(obj_in_cam, "world") - ``` - - Note: exactly one client, reused; do NOT create a connection per call; do NOT hardcode credentials (operator sets `VIAM_API_KEY`, `VIAM_API_KEY_ID`, `VIAM_MACHINE_FQDN` in the module's environment config). Close it on shutdown with `await self.robot_client.close()`. Reference: -- **Bridge callout:** side-by-side `from_robot` (local script) vs. `cast + get_resource_name` (module); resource names are identical in both. -- **`do_command` + scheduled job.** Cloud build time (~1 min for Python modules) stated upfront. - -**Verification:** run the four checks. `grep -n "FrameSystemClient" docs/tutorials/pick-and-place/06-inline-module.md` → **zero**. `grep -n "VIAM_MACHINE_FQDN\|create_robot_client_from_module" …` → hits. - -**Commit:** `docs(tutorials): author Phase 6 inline module with corrected in-module RobotClient` - ---- - -### Task 9: Final consistency sweep + full build - -**Goal:** Cross-page consistency and a clean full-site build. - -**Files:** all of `docs/tutorials/pick-and-place/` - -**Step 1: Cross-page grep sweep (expect zero hits each)** - -```bash -cd /Users/nick.hehr/src/viam-docs -grep -rniE "FrameSystemClient|point_cloud\.size|facilitator provides|pre-provisioned by instructor|sorted by color|red-bin|blue-bin|green-bin|does not exist yet|phase_total: 5|local-python-script" docs/tutorials/pick-and-place/ -``` - -**Step 2: Confirm the six-phase chain** - -```bash -grep -rn "phase:\|phase_total:\|prev:\|next:" docs/tutorials/pick-and-place/*.md -``` - -Verify: phases 1–6, all `phase_total: 6`, prev/next form the chain platform-mental-model → configure-resources → static-positions → control-the-robot-from-python → perception-guided-picking → inline-module. - -**Step 3: The four pre-PR checks (verbatim, in order)** - -```bash -cd /Users/nick.hehr/src/viam-docs -npx prettier --write "docs/tutorials/pick-and-place/**/*.md" -npx markdownlint-cli --config .markdownlint.yaml "docs/tutorials/pick-and-place/**/*.md" -vale sync && vale docs/tutorials/pick-and-place/ -make build-prod -``` - -Expected: prettier reformats in place; markdownlint clean; vale reports no errors; `make build-prod` completes without errors (old-date warnings OK). - -**Step 4: Browser spot-check (optional but recommended)** - -```bash -pkill -f "hugo server"; rm -rf public/ -hugo server --port 1313 --disableFastRender -``` - -Open each phase: `workshop-phases` box and sidebar show six phases in order with the current one highlighted; `workshop-nav` prev/next chains across all six; the `/tutorials/` landing card and `/tutorials/all/` archive are unaffected. - -**Step 5: Commit any sweep fixes** - -```bash -git add docs/tutorials/pick-and-place/ -git commit -m "docs(tutorials): final consistency sweep for six-phase self-serve workshop" -``` - ---- - -## Out of scope (deferred) - -- Companion-repo code changes (e.g. adding the home-pose guard clause to `reference-solution.py`). -- The hardware-setup how-to guide (`docs/guides/hardware-setup/xarm6-pick-and-place.md`). -- The `code-file` shortcode and `data/tutorials.yaml`. -- Header/hardware-overview imagery. -- Any change to the landing page, `/tutorials/all/` archive, or sidebar layout (2026-06-30 work). From 440352693faf960cf5830854bb9120bcfd1dabfb Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 16:08:03 -0400 Subject: [PATCH 43/44] docs(tutorials): add visual-asset placeholder comments Mark each screenshot, photo, diagram, and motion asset at its insertion point across the six phases. Shot list kept as local plans/ scratch. --- .../pick-and-place/01-platform-mental-model.md | 6 ++++++ .../pick-and-place/02-configure-resources.md | 14 ++++++++++++++ .../pick-and-place/03-static-positions.md | 14 ++++++++++++++ .../04-control-the-robot-from-python.md | 4 ++++ .../pick-and-place/05-perception-guided-picking.md | 13 +++++++++++++ docs/tutorials/pick-and-place/06-inline-module.md | 9 +++++++++ docs/tutorials/pick-and-place/_index.md | 6 ++++++ 7 files changed, 66 insertions(+) diff --git a/docs/tutorials/pick-and-place/01-platform-mental-model.md b/docs/tutorials/pick-and-place/01-platform-mental-model.md index 436bc5f730..d7e03c9244 100644 --- a/docs/tutorials/pick-and-place/01-platform-mental-model.md +++ b/docs/tutorials/pick-and-place/01-platform-mental-model.md @@ -30,6 +30,8 @@ You will not write any code or change any configuration in this phase. Instead, ## The three layers + + A Viam machine is made of three layers that each do one job: - **The Viam cloud app** is the source of truth for configuration. When you add a component, change an attribute, or wire up a service, you are editing a JSON document stored in the cloud. The app never runs your robot directly; it describes what should run. @@ -56,6 +58,8 @@ Open the **CONFIGURE** tab now and find the JSON view toggle near the top of the ## Resources: the universal abstraction + + Everything a Viam machine does, hardware and software alike, is modeled as a **resource**. Each resource has a name you choose (like `arm-1`), an API that describes what kind of thing it is (an arm, a camera, a vision service), and a model that identifies the specific implementation. Open the **CONFIGURE** tab and find `arm-1`. Next to its name you will see a triplet in the form `namespace:family:model`, for example `viam:ufactory:xArm6`. That triplet tells `viam-server` exactly which code to run for this resource: who published it (`namespace`), what family of hardware it belongs to (`family`), and the specific model (`model`). @@ -85,6 +89,8 @@ Open the **CONFIGURE** tab again and compare `arm-1`'s namespace to the namespac ## The resource dependency graph + + Resources can depend on each other. A gripper attaches to an arm, so `gripper-1`'s config points at `arm-1`. A vision service reads from a camera, so it depends on `cam-1`. `viam-server` reads these dependencies out of your config and builds a graph, then starts resources in an order that respects it: a resource cannot start until everything it depends on has started. This is the same pattern you will see again in Phase 5: `vision-segment` depends on `shape-detector`, which depends on `cam-1`. Neither can produce meaningful output until the resource beneath it is running. diff --git a/docs/tutorials/pick-and-place/02-configure-resources.md b/docs/tutorials/pick-and-place/02-configure-resources.md index c15b0f7e4c..27657d4b22 100644 --- a/docs/tutorials/pick-and-place/02-configure-resources.md +++ b/docs/tutorials/pick-and-place/02-configure-resources.md @@ -33,6 +33,10 @@ You will not touch the vision service in this phase. The `shape-detector` and `v ## Configure the arm + + + + On the **CONFIGURE** tab, add a new component. In the add-component dialog, search for `xArm6` and select the `viam:ufactory:xArm6` result. Name the component `arm-1`. Set the following attributes: @@ -50,6 +54,8 @@ The `viam:ufactory` module provides both the arm model and the gripper model you ## Configure the gripper + + Add another component. In the add-component dialog, search for the `viam:ufactory:gripper` model and select it. This model comes from the same `viam:ufactory` module as the arm. Name it `gripper-1`. Set one attribute: @@ -60,6 +66,8 @@ This attribute is also a dependency: `gripper-1` cannot start until `arm-1` is r ## Configure the camera + + Add a third component. In the add-component dialog, search for `realsense` and select the `viam:camera:realsense` result. Name it `cam-1`. Set the following attributes: @@ -72,6 +80,10 @@ Save the config and confirm in the **LOGS** tab that `viam-server` downloads the ## Test each resource from the CONTROL tab + + + + Open the **CONTROL** tab. You should now see a test card for each of the three components you just added. On the camera card, confirm you get a live feed: @@ -94,6 +106,8 @@ With a block between the fingers, **Grab** closes the fingers and the gripper ho ## The 3D scene tab + + Open the **3D scene** tab. You should see a rendered model of the arm and the `cam-1` frame positioned at the end of its wrist. Jog joint 1 with the arm card's sliders and watch the 3D scene as the arm turns. The `cam-1` frame moves with the arm, because the camera is mounted on the wrist rather than fixed in the workspace. diff --git a/docs/tutorials/pick-and-place/03-static-positions.md b/docs/tutorials/pick-and-place/03-static-positions.md index 4e8b305bb6..13e9f823df 100644 --- a/docs/tutorials/pick-and-place/03-static-positions.md +++ b/docs/tutorials/pick-and-place/03-static-positions.md @@ -38,6 +38,8 @@ The table below shows what each step in the static sequence validates: ## The key poses + + Each saved pose has a specific role in the sequence: | Pose | Purpose | @@ -52,6 +54,9 @@ The approach pose and the grasp pose share the same x and y coordinates. The onl ## 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 Phase 2. Add the `erh:vmodutils` module from the Viam registry to your machine. This module provides the `erh:vmodutils:arm-position-saver` switch model you use to save and recall poses, and the `erh:vmodutils:obstacle` model you use later in this phase. @@ -97,6 +102,8 @@ Obstacles are not a separate WorldState file you import. Each obstacle is an `er ### Measure your workspace + + Before you can fill in the obstacle geometry, measure your own table and workspace boundary. You need two kinds of measurement, and each one feeds a different part of the config: - **Tape-measure dimensions** for the box sizes: the table's length, width, and thickness go into the table obstacle's `x`, `y`, and `z`. Use the tape measure for how big each box is, not for where it sits. @@ -111,6 +118,8 @@ Each obstacle geometry is a box defined by its `x`, `y`, and `z` dimensions (the ### Add the table obstacle + + Add a component: - API: `rdk:component:gripper` @@ -141,6 +150,8 @@ Replace the `x`, `y`, and `z` dimensions with your own measured table length, wi ### Add the safety walls + + Add two more `erh:vmodutils:obstacle` components, one per boundary you want to wall off: ```json @@ -199,6 +210,9 @@ You can check your obstacle configuration against the companion repo's [obstacle ## Test the full static sequence + + + From the **CONTROL** tab, trigger the pose switches in this order: ```text diff --git a/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md b/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md index 6af9cb5623..7d4eb673f0 100644 --- a/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md +++ b/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md @@ -51,6 +51,8 @@ python3 starter-script.py ## Connect to your robot + + Open `starter-script.py` and find the `connect()` function. It mirrors the boilerplate the Viam app generates for you on the machine's **CONNECT** tab, under **Python SDK**: ```python @@ -101,6 +103,8 @@ You enable it in Phase 5 once the vision service exists. The `motion` handle is ## Run the script + + Run the script now with `uv run python starter-script.py`. It happens in a single run: `connect()` opens the connection, the script prints every resource on the machine, and then it immediately drives the arm through the static sequence. Watch the printed resource list scroll past in your terminal before the arm starts moving. The first thing printed is the full resource list: diff --git a/docs/tutorials/pick-and-place/05-perception-guided-picking.md b/docs/tutorials/pick-and-place/05-perception-guided-picking.md index 7874e6d89e..0a4d8eb3f6 100644 --- a/docs/tutorials/pick-and-place/05-perception-guided-picking.md +++ b/docs/tutorials/pick-and-place/05-perception-guided-picking.md @@ -21,6 +21,9 @@ In this phase you replace the fixed approach and grasp poses from Phase 4 with l ## Configure the vision pipeline + + + Phase 2 previewed two vision services and asked you to hold off configuring them. Add both now, in order, since the second depends on the first. Add a **vision** service: @@ -80,6 +83,8 @@ vision = VisionClient.from_robot(machine, "vision-segment") ## The frame system and transform_pose + + Every pose that `vision-segment` returns is expressed in the `cam-1` frame. That is the only frame the vision service knows about: it looked at pixels and depth values coming out of one camera, so the coordinates it hands back describe where a block sits relative to that camera's own origin and orientation. The motion service does not think in camera coordinates. It plans in the `world` frame, the same frame your obstacle geometry in Phase 3 was defined against. To hand a detected pose to the motion service, you first have to express it in `world` instead of `cam-1`. @@ -95,6 +100,8 @@ obj_in_world = await machine.transform_pose(obj_in_cam, "world") ## Detect from home (the wrist-camera rule) + + The `cam-1` frame is only a fixed thing to reason about when the arm is in a fixed pose. Because the camera is wrist-mounted, jogging any joint changes where `cam-1` sits in space, which changes what `transform_pose` reports for the exact same physical block. If you detect once with the arm near the bin and again with the arm hovering over the table, `transform_pose` gives two different world answers for two different arm positions, even if no block has moved at all. The fix is a rule, not a calculation: always move to `home-pose`, the observation position from Phase 3, before you call the vision service. Every detection in this workshop starts from the same known arm position, so `cam-1` always means the same thing when `transform_pose` reads it. @@ -131,6 +138,8 @@ Add a `print(obj_in_world.pose)` after the transform and run the script. Watch t ## Compute the approach and grasp poses + + Before you turn `obj_in_world.pose` into a place to move the gripper, it matters exactly what `motion.move` moves. Two motions that sound similar are not the same thing: - The CONTROL tab's arm card, and a direct `Arm` method, move the arm's own end frame: the flange at the end of the last joint. @@ -174,6 +183,8 @@ grasp_pose = offset_pose(obj_in_world.pose, GRIPPER_LENGTH_MM) ## Run the full pick loop + + With `approach_pose` and `grasp_pose` computed, assemble the full cycle: ```python @@ -232,6 +243,8 @@ This is a refinement, not a requirement for a working pick loop. Try the unconst ## Debugging guide + + Work through these in order. The first one causes most of the rest. If you get stuck, compare your loop against the complete [`reference-solution.py`](https://github.com/viam-devrel/pick-and-place/blob/main/scripts/reference-solution.py) in the companion repo. - **Did you detect from `home-pose`?** This is the first thing to check for nearly every perception symptom below. If the `await home.set_position(2)` guard is missing before a `get_object_point_clouds` call, or if you added a second detection somewhere that skips it, every downstream pose is computed against the wrong camera position. diff --git a/docs/tutorials/pick-and-place/06-inline-module.md b/docs/tutorials/pick-and-place/06-inline-module.md index 5dfb0321c7..1a2fe6f450 100644 --- a/docs/tutorials/pick-and-place/06-inline-module.md +++ b/docs/tutorials/pick-and-place/06-inline-module.md @@ -44,6 +44,9 @@ Two tiers, so you can stop at whichever one matches what you came here for: ## Open the inline module editor + + + Before you start pasting code, know what to expect: saving an inline Python module triggers a cloud build, and that build takes about a minute. It is not instant the way rerunning a local script is, so give it that minute rather than assuming a save failed. Open your machine's **CONFIGURE** tab and add a new module. Choose to create a local module with an inline editor rather than pulling one from the registry, and select Python as the language. The Viam app opens a code editor in your browser with a generated module skeleton, so there is no local project setup to do first. @@ -56,6 +59,8 @@ The module finishes its cloud build and starts without errors in the **LOGS** ta ## Dependency injection + + A script builds its resource handles once, right after it connects, by calling `Arm.from_robot(machine, "arm-1")` and similar for each resource it needs. A module does not connect to itself, so it cannot call `from_robot` the same way. Instead, the module framework hands your module its dependencies. Two lifecycle methods carry this pattern: @@ -77,6 +82,8 @@ Keep the rest of your `reconfigure` close to this shape: look up each resource y ## The frame system from inside a module + + This is the one genuine change from Phase 5, so read it carefully even if you skim the rest of this phase. No injected dependency gives you frame-system access the way `dependencies[Arm.get_resource_name("arm-1")]` gives you the arm; there is no such dependency to inject. `transform_pose` lives on the machine-management API, and a module reaches that API the same way a script does: through a `RobotClient`. The difference is that a module has to build that `RobotClient` itself, from credentials in its own environment, rather than receiving one as a dependency. @@ -121,6 +128,8 @@ The resource name, `"arm-1"`, is identical in both, and the same is true of `gri ## do_command and a scheduled job + + With dependencies wired up and `transform_pose` reachable, assemble your Phase 5 pick-and-place logic into a single method on the module, the same detection, transform, pose math, and motion calls, unchanged. What differs is how that method gets triggered. For the minimal viable module, trigger it through `do_command`. `do_command` is a generic handler every module exposes for commands that do not fit the typed component or service APIs. A small illustrative sketch: diff --git a/docs/tutorials/pick-and-place/_index.md b/docs/tutorials/pick-and-place/_index.md index ae897ed33d..3cf5845a6c 100644 --- a/docs/tutorials/pick-and-place/_index.md +++ b/docs/tutorials/pick-and-place/_index.md @@ -29,6 +29,8 @@ The workshop is structured as six sequential phases, each ending with a working ## What you'll build + + You will configure an xArm6 robotic arm fitted with a finger gripper and a wrist-mounted Intel RealSense depth camera. A shape-detection vision service finds blocks in camera space, and the Viam motion service plans and executes collision-free picks that place each block in the bin. By the end of Phase 5 you have a Python script you run from your laptop that drives the full detect-pick-place loop. @@ -62,6 +64,8 @@ This is a self-serve workshop, so confirm each of the following before you start ### Validate your environment + + Before starting Phase 4, confirm your environment is ready: ```sh @@ -73,6 +77,8 @@ If either command fails, revisit the checklist above before continuing. ### Where to start + + - **Physical hardware ready:** start at [Phase 1](/tutorials/pick-and-place/platform-mental-model/). - **Provisioning your own hardware:** complete the hardware setup guide first (forthcoming), then return here for [Phase 1](/tutorials/pick-and-place/platform-mental-model/). From 2a45b04084d57fb6cc6af46eeba25b6543c7c8f3 Mon Sep 17 00:00:00 2001 From: HipsterBrown Date: Thu, 2 Jul 2026 16:27:59 -0400 Subject: [PATCH 44/44] fix(tutorials): alias renamed Phase 4 URL and clean up Python snippets Netlify's no-more-404 plugin failed the deploy preview because renaming 04-local-python-script -> 04-control-the-robot-from-python left the old preview URL /tutorials/pick-and-place/local-python-script/ with no target. Add a Hugo alias so the old path redirects to the new slug. Also fix the flake8-markdown check: add two blank lines before the def in three snippets (E302), and ignore F706 (return outside function) alongside the already-ignored F704 (await outside function), since these docs show partial async snippets by design. --- .flake8 | 2 +- .../pick-and-place/04-control-the-robot-from-python.md | 3 +++ docs/tutorials/pick-and-place/06-inline-module.md | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index ed4a6ff6f3..a1cd0aa7f8 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -extend-ignore = F704, F821, F401, F841, E125, E117, F403, E999, E501, F405 +extend-ignore = F704, F706, F821, F401, F841, E125, E117, F403, E999, E501, F405 exclude = .git,build,dist max-complexity = 20 diff --git a/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md b/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md index 7d4eb673f0..4696f304f1 100644 --- a/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md +++ b/docs/tutorials/pick-and-place/04-control-the-robot-from-python.md @@ -12,6 +12,8 @@ phase_total: 6 time_estimate: "15 minutes" prev: "/tutorials/pick-and-place/static-positions/" next: "/tutorials/pick-and-place/perception-guided-picking/" +aliases: + - /tutorials/pick-and-place/local-python-script/ languages: ["python"] --- @@ -60,6 +62,7 @@ MACHINE_ADDRESS = "" API_KEY = "" API_KEY_ID = "" + async def connect() -> RobotClient: return await RobotClient.at_address( MACHINE_ADDRESS, diff --git a/docs/tutorials/pick-and-place/06-inline-module.md b/docs/tutorials/pick-and-place/06-inline-module.md index 1a2fe6f450..822b2a8fcc 100644 --- a/docs/tutorials/pick-and-place/06-inline-module.md +++ b/docs/tutorials/pick-and-place/06-inline-module.md @@ -74,6 +74,7 @@ A small illustrative sketch of that mapping, not a complete implementation: from typing import cast from viam.components.arm import Arm + def reconfigure(self, config, dependencies): self.arm = cast(Arm, dependencies[Arm.get_resource_name("arm-1")]) ``` @@ -94,6 +95,7 @@ Use this pattern exactly as written: import os from viam.robot.client import RobotClient + async def create_robot_client_from_module(): opts = RobotClient.Options.with_api_key( api_key=os.environ["VIAM_API_KEY"],