<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: vmodal_ai</title>
    <description>The latest articles on DEV Community by vmodal_ai (@vmodal_ai).</description>
    <link>https://dev.to/vmodal_ai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4052446%2F2fcb63ba-7be8-4ffd-93ce-f4f00223ddfa.jpeg</url>
      <title>DEV Community: vmodal_ai</title>
      <link>https://dev.to/vmodal_ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vmodal_ai"/>
    <language>en</language>
    <item>
      <title>Building a Vision-Language-Action Robot System from Scratch</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:20:43 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/building-a-vision-language-action-robot-system-from-scratch-1g78</link>
      <guid>https://dev.to/vmodal_ai/building-a-vision-language-action-robot-system-from-scratch-1g78</guid>
      <description>&lt;h1&gt;
  
  
  Building a Vision-Language-Action Robot System from Scratch
&lt;/h1&gt;

&lt;p&gt;Vision-Language-Action (VLA) systems connect visual perception, natural-language instructions, and physical robot actions.&lt;/p&gt;

&lt;p&gt;Instead of requiring a user to specify low-level commands such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Move forward 2 meters.
Turn left 90 degrees.
Open gripper.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;a VLA system can accept a higher-level instruction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Pick up the red box and place it on the table."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system then translates that intent into a sequence of robot actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  VLA Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             User Instruction
                    ↓
             Language Model
                    ↓
             Task Planner
                    ↓
        ┌───────────┴───────────┐
        ↓                       ↓
   Vision System           World Model
        └───────────┬───────────┘
                    ↓
             Action Planner
                    ↓
              Safety Layer
                    ↓
                Control
                    ↓
                Robot
                    ↑
                 Sensors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important design principle is that language and vision models should not directly bypass the robot's safety and control layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the Robot Action Space
&lt;/h2&gt;

&lt;p&gt;Start with a small set of deterministic actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;navigate(location)
look_at(object)
pick(object)
place(location)
open_gripper()
close_gripper()
stop()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is safer than allowing a model to generate arbitrary motor commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Capture Visual Input
&lt;/h2&gt;

&lt;p&gt;A camera publishes frames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/camera/image_raw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A vision pipeline can detect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;red_box
table
person
floor
obstacle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The perception system should transform raw images into structured objects.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"objects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"red_box"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Convert Language into a Task
&lt;/h2&gt;

&lt;p&gt;The language model interprets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Pick up the red box."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into a structured task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"goal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pick"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"red_box"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid passing free-form model output directly to robot hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Ground Language in the World
&lt;/h2&gt;

&lt;p&gt;Language references must be connected to visual objects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"red box"
    ↓
Language Entity
    ↓
Vision Detections
    ↓
Object ID: object_17
    ↓
3D Position
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This process is often called grounding.&lt;/p&gt;

&lt;p&gt;A robust system should consider ambiguity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: "Pick up the box."

Detected:
- box_01
- box_02
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system should request clarification or use an explicit selection policy rather than guessing when the consequences are significant.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Build a Task Planner
&lt;/h2&gt;

&lt;p&gt;Break a high-level task into executable steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pick up red box
      ↓
Navigate to box
      ↓
Locate box
      ↓
Approach box
      ↓
Align gripper
      ↓
Close gripper
      ↓
Verify grasp
      ↓
Navigate to table
      ↓
Place box
      ↓
Verify placement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The planner can be implemented using state machines, behavior trees, or another explicit orchestration mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Integrate ROS 2
&lt;/h2&gt;

&lt;p&gt;ROS 2 provides the communication layer between perception, planning, and control components.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VLA Node
   ↓
/task_goal
   ↓
Task Planner
   ↓
/navigation_goal
   ↓
Navigation
   ↓
/cmd_vel
   ↓
Robot Base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For long-running operations such as navigation, ROS 2 actions provide goal, feedback, result, and cancellation semantics. &lt;a href="https://docs.ros.org/en/rolling/Concepts/Basic/Interfaces-Topics-Services-Actions.html" rel="noopener noreferrer"&gt;ROS 2 actions&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Add a Safety Layer
&lt;/h2&gt;

&lt;p&gt;The VLA model may propose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"move"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"velocity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The safety layer should reject invalid values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;velocity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_SAFE_SPEED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;emergency_stop_active&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model should never be the final authority over physical safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Add Action Verification
&lt;/h2&gt;

&lt;p&gt;After every important action, verify the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pick Action
    ↓
Gripper Close
    ↓
Visual Verification
    ↓
Object Lifted?
   ├── Yes → Continue
   └── No  → Retry / Recover
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a closed-loop VLA system.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Handle Uncertainty
&lt;/h2&gt;

&lt;p&gt;Vision and language models are probabilistic.&lt;/p&gt;

&lt;p&gt;Represent confidence explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Object confidence: 0.94
Language grounding: 0.91
Grasp confidence: 0.72
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use thresholds and fallback behaviors appropriate to the task.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Keep the VLA Layer Replaceable
&lt;/h2&gt;

&lt;p&gt;A clean architecture allows the language or vision model to change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ┌───────────────┐
Camera ─────────→│ Vision Model  │
                 └───────┬───────┘
                         ↓
                   World Model
                         ↑
                 ┌───────┴───────┐
User ───────────→│ Language Model│
                 └───────────────┘
                         ↓
                   Task Planner
                         ↓
                   ROS 2 Actions
                         ↓
                     Control
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the robot should not depend on a specific model vendor or model architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Example End-to-End Flow
&lt;/h2&gt;

&lt;p&gt;Suppose the user says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Take the red box to the charging station."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Speech / Text Input
2. Language Understanding
3. Identify "red box"
4. Vision Detection
5. Ground Language → Object
6. Generate Task Plan
7. Navigate to Object
8. Approach Object
9. Grasp Object
10. Verify Grasp
11. Navigate to Charging Station
12. Place Object
13. Verify Result
14. Report Completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is substantially more robust than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt → LLM → Motor Commands
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  12. Production Considerations
&lt;/h2&gt;

&lt;p&gt;A real VLA robot should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Model versioning&lt;/li&gt;
&lt;li&gt;Prompt/version management&lt;/li&gt;
&lt;li&gt;Sensor synchronization&lt;/li&gt;
&lt;li&gt;Timeouts&lt;/li&gt;
&lt;li&gt;Safety watchdogs&lt;/li&gt;
&lt;li&gt;Human override&lt;/li&gt;
&lt;li&gt;Action validation&lt;/li&gt;
&lt;li&gt;Audit logs&lt;/li&gt;
&lt;li&gt;Simulation tests&lt;/li&gt;
&lt;li&gt;Failure recovery&lt;/li&gt;
&lt;li&gt;Offline fallback behaviors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For mobile robots, Nav2 can provide the navigation layer beneath the higher-level task planner. &lt;a href="https://docs.nav2.org/" rel="noopener noreferrer"&gt;Nav2&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  13. Build Incrementally
&lt;/h2&gt;

&lt;p&gt;A practical development roadmap is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Phase 1
Vision → Object Detection

Phase 2
Language → Structured Tasks

Phase 3
Task → Deterministic Robot Actions

Phase 4
Vision + Language Grounding

Phase 5
Closed-Loop Verification

Phase 6
Safety + Recovery

Phase 7
Production Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not start with a fully autonomous general-purpose robot. Start with a small, measurable action space and expand it gradually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A Vision-Language-Action system is best understood as a layered robotics architecture in which AI provides perception and high-level reasoning while deterministic robotics software handles planning, validation, safety, and control.&lt;/p&gt;

&lt;p&gt;The result is a robot that can understand natural-language goals, connect those goals to what its sensors observe, execute physical actions, and verify whether those actions actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="http://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;https://github.com/v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;https://github.com/v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Integrating Perception, Planning, and Control in Autonomous Robots</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:20:08 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/integrating-perception-planning-and-control-in-autonomous-robots-28e6</link>
      <guid>https://dev.to/vmodal_ai/integrating-perception-planning-and-control-in-autonomous-robots-28e6</guid>
      <description>&lt;h1&gt;
  
  
  Integrating Perception, Planning, and Control in Autonomous Robots
&lt;/h1&gt;

&lt;p&gt;Autonomous robots need to continuously answer three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What is around me?&lt;/strong&gt; — Perception&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What should I do?&lt;/strong&gt; — Planning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do I physically do it?&lt;/strong&gt; — Control&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These systems become powerful when integrated into a closed feedback loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Autonomy Loop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       ┌───────────────┐
       │    Sensors    │
       └───────┬───────┘
               ↓
       ┌───────────────┐
       │  Perception   │
       └───────┬───────┘
               ↓
       ┌───────────────┐
       │ World / State │
       └───────┬───────┘
               ↓
       ┌───────────────┐
       │   Planning    │
       └───────┬───────┘
               ↓
       ┌───────────────┐
       │   Control     │
       └───────┬───────┘
               ↓
            Robot
               │
               └──── feedback ────→ Sensors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Perception
&lt;/h2&gt;

&lt;p&gt;Perception converts sensor measurements into estimates.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Camera Frame
     ↓
Object Detector
     ↓
Person: 0.96
Box: 0.91
Chair: 0.87
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For navigation, perception may generate obstacles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Obstacle A → x=2.0, y=0.5
Obstacle B → x=3.2, y=-1.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always associate detections with timestamps and, where possible, uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. State Estimation
&lt;/h2&gt;

&lt;p&gt;Planning should not consume isolated detections. It needs a coherent state.&lt;/p&gt;

&lt;p&gt;Combine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IMU
Wheel Odometry
GPS
LiDAR
Camera
     ↓
Sensor Fusion
     ↓
Robot Pose + Velocity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The state estimator should continuously update the robot's belief about its position and motion.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Planning
&lt;/h2&gt;

&lt;p&gt;Planning transforms the current state and goal into a desired behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;State + Goal + Environment
            ↓
         Planner
            ↓
      Desired Path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For navigation systems, Nav2 provides planning and control infrastructure for ROS 2 robots. &lt;a href="https://docs.nav2.org/" rel="noopener noreferrer"&gt;Nav2 documentation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Control
&lt;/h2&gt;

&lt;p&gt;The controller follows the planned trajectory.&lt;/p&gt;

&lt;p&gt;For a differential-drive robot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Desired Path
     ↓
Controller
     ↓
linear velocity
angular velocity
     ↓
Motor Driver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A controller should operate at a predictable frequency and enforce physical limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Connect Everything with ROS 2
&lt;/h2&gt;

&lt;p&gt;A simplified ROS graph could be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;camera_node
     ↓
/image
     ↓
perception_node
     ↓
/detections
     ↓
world_model_node
     ↓
/world_state
     ↓
planner_node
     ↓
/planned_path
     ↓
controller_node
     ↓
/cmd_vel
     ↓
base_controller
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ROS 2 topics are appropriate for continuous streams such as sensor data and robot state. Actions are useful for long-running navigation or other behaviors requiring feedback. &lt;a href="https://docs.ros.org/en/rolling/Concepts/Basic/Interfaces-Topics-Services-Actions.html" rel="noopener noreferrer"&gt;ROS 2 interfaces&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Avoid Blocking the Control Loop
&lt;/h2&gt;

&lt;p&gt;Suppose object detection takes 200 ms.&lt;/p&gt;

&lt;p&gt;Do not make the motor control loop wait for inference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bad:
Control → wait for AI → Control
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Perception: asynchronous
Planning: asynchronous
Control: deterministic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller should continue operating with the latest valid state.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Add Confidence and Freshness
&lt;/h2&gt;

&lt;p&gt;A perception result should have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;value
confidence
timestamp
source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;Detection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;person&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;123456789&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reject stale information when it is no longer safe to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Add Recovery Behaviors
&lt;/h2&gt;

&lt;p&gt;Autonomous systems should expect failures.&lt;/p&gt;

&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Path blocked
   ↓
Replan
   ↓
Still blocked?
   ↓
Recovery behavior
   ↓
Stop / Request Help
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recovery behavior is part of autonomy—not an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Example Control Pipeline
&lt;/h2&gt;

&lt;p&gt;A mobile robot can implement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;control&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;select_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pose&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_linear_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;angular&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_angular_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;MAX_V&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_V&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;angular&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angular&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;MAX_W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_W&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;angular&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller receives a safe representation of the planned path and produces bounded commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Measure End-to-End Latency
&lt;/h2&gt;

&lt;p&gt;Measure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sensor timestamp
      ↓
Perception timestamp
      ↓
Planning timestamp
      ↓
Control timestamp
      ↓
Actuator timestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important metric is not just model inference time. It is the age of the information when the actuator receives the command.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Simulation and Testing
&lt;/h2&gt;

&lt;p&gt;Start with recorded data or simulation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recorded Sensors
       ↓
Perception
       ↓
Planning
       ↓
Control
       ↓
Simulation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then move to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Simulation → Hardware-in-the-Loop → Real Robot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test edge cases such as missing detections, moving obstacles, localization loss, and planner failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Perception, planning, and control should be treated as a continuous feedback system.&lt;/p&gt;

&lt;p&gt;Perception tells the robot what it believes is happening. Planning chooses an appropriate response. Control turns that response into physical motion. The loop repeats as new sensor information arrives.&lt;/p&gt;

&lt;p&gt;The key engineering principle is to keep the control layer predictable while allowing AI-heavy perception and reasoning components to evolve independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="http://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;https://github.com/v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;https://github.com/v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>computerscience</category>
      <category>robotics</category>
    </item>
    <item>
      <title>Creating a Robot Sensor Data Recording and Replay System</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:19:44 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/creating-a-robot-sensor-data-recording-and-replay-system-37h0</link>
      <guid>https://dev.to/vmodal_ai/creating-a-robot-sensor-data-recording-and-replay-system-37h0</guid>
      <description>&lt;h1&gt;
  
  
  Creating a Robot Sensor Data Recording and Replay System
&lt;/h1&gt;

&lt;p&gt;A robust recording-and-replay system is the backbone of any serious robot learning workflow. Recording lets you build datasets and debug incidents after the fact; replay lets you re-run recorded sensor streams through your perception or control stack without needing the physical robot, which massively speeds up development and debugging. This tutorial covers building both halves as a cohesive system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why replay matters as much as recording
&lt;/h2&gt;

&lt;p&gt;It's easy to treat recording as "just logging," but a good replay system pays for itself quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debugging without hardware&lt;/strong&gt;: reproduce a bug from last week's session without booking robot time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regression testing&lt;/strong&gt;: replay a fixed set of recorded sessions through a new version of your perception pipeline and diff the outputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset iteration&lt;/strong&gt;: re-extract features, re-label, or re-sample previously recorded sessions as your pipeline evolves, without recollecting data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulation grounding&lt;/strong&gt;: compare simulated sensor output against real recorded sensor output for the same nominal trajectory.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Core design: a session as a set of synchronized streams
&lt;/h2&gt;

&lt;p&gt;Model a recording session as multiple independent streams (camera, joint states, IMU, force-torque, etc.), each with its own timestamped samples, plus a session-level index that lets you query "what did every stream look like at time T."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Session/
  metadata.json          # session info: robot config, sensors present, start time
  streams/
    camera_wrist/
      timestamps.npy
      frames/            # or a video file + frame index
    joint_states/
      timestamps.npy
      data.npy
    gripper/
      timestamps.npy
      data.npy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The recorder
&lt;/h2&gt;

&lt;p&gt;The recorder's job is simple in principle: subscribe to every sensor stream, timestamp each sample, and write it to disk without blocking the control loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StreamRecorder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Called from the sensor callback thread — must be fast and non-blocking
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;running&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design point: &lt;code&gt;record()&lt;/code&gt; is called from whatever thread the sensor callback lives on, and it must return immediately. All the actual disk I/O happens on a dedicated writer thread per stream. This decoupling is what prevents a slow disk write from stalling your control loop — a very common source of "why does my robot judder every few seconds" bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling different sensor rates cleanly
&lt;/h2&gt;

&lt;p&gt;Cameras, joint encoders, and force-torque sensors rarely run at the same rate. Rather than forcing everything onto one clock during recording, record each stream at its native rate with accurate timestamps, and defer alignment to query time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_nearest_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;searchsorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;query_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;query_time&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This "query by nearest timestamp, per stream" pattern is more flexible than forcing a single global sample rate at recording time — it lets you resample to whatever rate a downstream consumer (a training pipeline, a replay tool) actually needs, without having thrown away information at recording time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the replay engine
&lt;/h2&gt;

&lt;p&gt;A replay engine reconstructs a session and plays it back through the same interfaces your live system uses, so your downstream code doesn't need separate "live" and "replay" code paths.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SessionReplayer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;streams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_load_streams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load_streams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Load timestamps + data arrays per stream from disk
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;NotImplementedError&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_state_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_nearest_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;streams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.033&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_time&lt;/span&gt;
        &lt;span class="n"&gt;wall_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_state_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;
            &lt;span class="n"&gt;target_wall_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wall_start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;
            &lt;span class="n"&gt;sleep_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_wall_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sleep_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sleep_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By designing the &lt;code&gt;callback(state, t)&lt;/code&gt; interface to match what your live perception/control code expects as input, you can point the exact same downstream code at either live sensors or a replayed session — this is the single biggest productivity win of building a proper replay system rather than ad hoc debug scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical tips for a system that scales
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compress images as you go&lt;/strong&gt;, not after the fact — writing raw uncompressed frames during a live session will exhaust disk bandwidth quickly on any camera above VGA resolution. JPEG or a lightweight video codec (H.264) with a keyframe interval short enough for random access works well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write an index file per session&lt;/strong&gt; (start/end time, sensors present, robot config, any metadata like task label or operator id) so you can filter and search sessions without opening every stream file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version your schema.&lt;/strong&gt; Sensor configurations change over a project's lifetime — added cameras, changed resolutions, new sensors. Store a schema version in each session's metadata so your loader can handle old and new sessions correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate on write, not just on read.&lt;/strong&gt; Catch dropped frames, clock jumps, and sensor disconnects at recording time when you can still restart the session, rather than discovering the corruption weeks later during training.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this fits in the bigger picture
&lt;/h2&gt;

&lt;p&gt;This recording-and-replay system is the shared infrastructure underneath every other tutorial in this series — teleoperation sessions, human demonstrations, and imitation learning training data are all, at the storage layer, just sessions recorded and replayed through this same system. Getting this layer right early pays dividends across the entire robot learning pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="https://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>ai</category>
      <category>python</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Using VR Controllers for Robot Teleoperation</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:19:40 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/using-vr-controllers-for-robot-teleoperation-54gh</link>
      <guid>https://dev.to/vmodal_ai/using-vr-controllers-for-robot-teleoperation-54gh</guid>
      <description>&lt;h1&gt;
  
  
  Using VR Controllers for Robot Teleoperation
&lt;/h1&gt;

&lt;p&gt;VR controllers (Meta Quest, HTC Vive, Valve Index) have become a popular teleoperation input for robot learning, and for good reason: they provide full 6-DOF pose tracking, intuitive hand-based control, and built-in buttons/triggers that map naturally onto gripper control — all in a consumer-grade, relatively cheap package. This tutorial covers how to set up VR controllers as a teleoperation input for a robot arm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why VR controllers work well for teleoperation
&lt;/h2&gt;

&lt;p&gt;Compared to a gamepad or keyboard, a VR controller gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous 6-DOF pose&lt;/strong&gt; (position + orientation) tracked at high frequency, which maps intuitively to end-effector pose control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural hand motion&lt;/strong&gt;, so operators produce trajectories that look like real human manipulation rather than artificial waypoint sequences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in analog trigger&lt;/strong&gt;, perfect for continuous gripper open/close control instead of a binary toggle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Haptic feedback&lt;/strong&gt; (on some controllers), useful for signaling contact or constraint violations back to the operator.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting controller pose data
&lt;/h2&gt;

&lt;p&gt;Most VR SDKs (OpenXR, Meta's Oculus SDK, SteamVR) expose controller pose as a position + quaternion relative to the headset's tracking origin, updated at high frequency (often 60–120 Hz). A minimal OpenXR-style polling loop looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VRControllerInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xr_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;controller_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xr_session&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;controller_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;controller_path&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_controller_pose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;controller_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;trigger_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_input_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;controller_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trigger&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;grip_button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_input_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;controller_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# (x, y, z)
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orientation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pose&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orientation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# quaternion (x, y, z, w)
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trigger&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trigger_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# 0.0 - 1.0, maps to gripper
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grip_button&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grip_button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# engage/disengage teleop
&lt;/span&gt;        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using Unity or Unreal for the VR side and streaming to a Python robot controller, this same pose data typically gets sent over a lightweight transport like ZeroMQ, gRPC, or a WebSocket, since VR engines and robotics stacks (ROS, Python control loops) usually don't live in the same process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping controller pose to robot end-effector pose
&lt;/h2&gt;

&lt;p&gt;The core challenge is retargeting: the VR controller's coordinate frame and workspace scale rarely match the robot's. A typical approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Establish a reference frame.&lt;/strong&gt; When the operator engages the grip button, record the controller's current pose as the origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute relative motion.&lt;/strong&gt; For every subsequent frame, compute the controller's pose &lt;em&gt;relative to that origin&lt;/em&gt;, not its absolute pose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply a scale factor.&lt;/strong&gt; Human arm movement range is often larger than the robot's comfortable workspace (or vice versa), so scale the relative translation before applying it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the relative motion to the robot's pose at engagement time&lt;/strong&gt;, producing the new target pose.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy.spatial.transform&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Rotation&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VRToRobotMapper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position_scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position_scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;position_scale&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engaged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_controller_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_controller_rot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_robot_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_robot_rot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;engage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;controller_pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;controller_rot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;robot_pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;robot_rot&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engaged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_controller_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller_pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_controller_rot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_quat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller_rot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_robot_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;robot_pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_robot_rot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_quat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;robot_rot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;disengage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engaged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;controller_pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;controller_rot&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;engaged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

        &lt;span class="n"&gt;delta_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller_pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_controller_pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position_scale&lt;/span&gt;
        &lt;span class="n"&gt;target_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_robot_pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;delta_pos&lt;/span&gt;

        &lt;span class="n"&gt;delta_rot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_controller_rot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_quat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller_rot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;target_rot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin_robot_rot&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;delta_rot&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;target_pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_rot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_quat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This "clutch" pattern — engage, move, disengage, reposition your hand, re-engage — mirrors how a mouse works when you lift it and reposition mid-drag. It's essential once the human's comfortable arm range doesn't match the robot's workspace, which is almost always the case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gripper control from the trigger
&lt;/h2&gt;

&lt;p&gt;Map the analog trigger value directly to a gripper closure percentage rather than treating it as a binary switch — this preserves fine-grained grasp force control, which is valuable both for the task itself and, later, as a richer training signal for imitation learning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;trigger_to_gripper_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trigger_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# trigger_value: 0.0 (released) to 1.0 (fully pressed)
&lt;/span&gt;    &lt;span class="n"&gt;closure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trigger_value&lt;/span&gt;  &lt;span class="c1"&gt;# 0 = open, 1 = fully closed
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;min_open&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;closure&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_open&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;min_open&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling orientation carefully
&lt;/h2&gt;

&lt;p&gt;A common bug: naively slerp-ing or averaging quaternions without checking for the double-cover property of quaternions (q and -q represent the same rotation) causes sudden 180-degree "flips" in the mapped orientation. Always normalize sign consistency before interpolating, or use rotation matrices/axis-angle deltas if you're not confident in your quaternion math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Latency and jitter considerations
&lt;/h2&gt;

&lt;p&gt;VR tracking is generally very good, but a few things to watch for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filter trigger and pose noise&lt;/strong&gt; with a light exponential moving average — raw VR tracking data can have small high-frequency jitter that's imperceptible to the eye in a headset but shows up clearly in logged action data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account for network latency&lt;/strong&gt; if the VR engine and robot controller run in separate processes or machines — a delay of even 50–100 ms is noticeable as sluggish control and will show up as lag between the logged input and the resulting robot motion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decouple render rate from control rate.&lt;/strong&gt; The VR headset render loop typically runs faster than your robot control loop needs; sample the controller pose at your fixed control rate rather than trying to match VR frame rate exactly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Combining with the rest of the pipeline
&lt;/h2&gt;

&lt;p&gt;Everything from the general teleoperation and demonstration-capture tutorials earlier in this series applies directly here — the VR controller is simply a richer, more intuitive input device sitting at the top of the same architecture. Once your mapping and gripper control are solid, this setup tends to produce noticeably smoother, more natural demonstrations than joystick- or keyboard-based teleoperation, which pays off directly in imitation learning policy quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="https://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>ai</category>
      <category>vr</category>
      <category>python</category>
    </item>
    <item>
      <title>Building an Imitation Learning Pipeline for Robotic Manipulation</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:19:36 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/building-an-imitation-learning-pipeline-for-robotic-manipulation-3fmm</link>
      <guid>https://dev.to/vmodal_ai/building-an-imitation-learning-pipeline-for-robotic-manipulation-3fmm</guid>
      <description>&lt;h1&gt;
  
  
  Building an Imitation Learning Pipeline for Robotic Manipulation
&lt;/h1&gt;

&lt;p&gt;With a validated demonstration dataset in hand, the next step is building the actual training pipeline: turning (observation, action) pairs into a policy that can control the robot on its own. This tutorial covers the end-to-end pipeline — data loading, model architecture choices, training, and evaluation — for behavior-cloning-style imitation learning on manipulation tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipeline overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw Episodes -&amp;gt; Preprocessing -&amp;gt; Dataset/Dataloader -&amp;gt; Policy Model -&amp;gt; Training Loop -&amp;gt; Evaluation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage has decisions that materially affect final policy quality, so it's worth treating this as a real pipeline with clear interfaces, not a single monolithic script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Preprocessing
&lt;/h2&gt;

&lt;p&gt;Before training, normalize your data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Action normalization&lt;/strong&gt;: scale actions to roughly [-1, 1] using dataset statistics (mean/std or min/max). Unnormalized actions with wildly different scales across joints slow down and destabilize training.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image preprocessing&lt;/strong&gt;: resize to a consistent resolution, normalize pixel values, and optionally apply light augmentation (color jitter, random crop) to improve robustness — but avoid augmentations that break task-relevant geometry, like aggressive rotation for tasks sensitive to orientation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action representation&lt;/strong&gt;: decide between predicting absolute targets vs. delta actions vs. velocity commands. Delta actions relative to the current state are usually easier to learn and more robust to compounding drift at inference time.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActionNormalizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1e-6&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;denormalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Dataset and dataloader
&lt;/h2&gt;

&lt;p&gt;Structure your dataset so each sample is a short window of context, not just a single frame — most manipulation policies benefit from a few frames of history to disambiguate velocity and resolve partial occlusion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;torch.utils.data&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ManipulationDataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obs_horizon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action_horizon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;episodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;episodes&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;obs_horizon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obs_horizon&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_horizon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action_horizon&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_build_index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_build_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ep_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ep&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;obs_horizon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_horizon&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ep_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__len__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__getitem__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ep_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;ep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ep_i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;obs_seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;obs_horizon&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;action_seq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_horizon&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;obs_seq&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joint_state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;obs_seq&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joint_state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;action_seq&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Predicting a short &lt;em&gt;sequence&lt;/em&gt; of future actions (action chunking) rather than a single next action, and executing several before re-planning, tends to produce noticeably smoother behavior than pure single-step prediction — this is one of the more impactful architectural choices in recent manipulation policy designs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Model architecture
&lt;/h2&gt;

&lt;p&gt;A reasonable baseline architecture for image-based manipulation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A visual encoder (a small CNN or a pretrained ResNet backbone) processes each camera frame into a feature vector.&lt;/li&gt;
&lt;li&gt;Visual features are concatenated with proprioceptive state (joint positions/velocities).&lt;/li&gt;
&lt;li&gt;A sequence model (transformer or a simple MLP with the obs_horizon flattened) maps the combined features to a predicted action chunk.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch.nn&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ManipulationPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visual_encoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state_dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action_dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action_horizon&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visual_encoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visual_encoder&lt;/span&gt;
        &lt;span class="n"&gt;feature_dim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visual_encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_dim&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;state_dim&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature_dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ReLU&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action_dim&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;action_horizon&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_dim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action_dim&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_horizon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;action_horizon&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;joint_state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;visual_feat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visual_encoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;combined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;visual_feat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;joint_state&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_horizon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action_dim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more advanced setups, diffusion-based action heads (predicting the action chunk via denoising) tend to model multimodal human behavior — where the same task can reasonably be solved multiple ways — better than a plain MLP regression head, at the cost of more complex training and slower inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Training loop
&lt;/h2&gt;

&lt;p&gt;Standard supervised regression training, with mean squared error (or smooth L1) between predicted and demonstrated actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;train_epoch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dataloader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;total_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataloader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;joint_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joint_state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;target_actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;pred_actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;joint_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;functional&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;smooth_l1_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pred_actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_actions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero_grad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;backward&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;total_loss&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total_loss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataloader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few practical tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track validation loss on held-out &lt;em&gt;episodes&lt;/em&gt;, not held-out frames — random frame-level splits leak information between train and val since adjacent frames are highly correlated.&lt;/li&gt;
&lt;li&gt;Watch for the gap between training loss and real-world performance: low loss doesn't guarantee good closed-loop behavior, since imitation learning is an open-loop training objective applied to a closed-loop control problem (compounding error is the classic failure mode here).&lt;/li&gt;
&lt;li&gt;Use early stopping based on a small set of real or simulated rollout evaluations if at all possible, not just loss curves.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Evaluation
&lt;/h2&gt;

&lt;p&gt;Loss curves only tell part of the story. Evaluate with actual closed-loop rollouts on the robot (or in simulation) and track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Task success rate&lt;/strong&gt; across multiple trials and initial conditions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery behavior&lt;/strong&gt; — does the policy handle small perturbations gracefully, or does any deviation from the training distribution cause it to fail catastrophically?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smoothness&lt;/strong&gt; — jerky, high-frequency action outputs often indicate the policy hasn't generalized well and is essentially "hunting" between similar training examples.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;This pipeline covers the core behavior-cloning loop. From here, natural extensions include adding more demonstration diversity, experimenting with different action representations, or moving to more advanced input devices like VR controllers to make demonstration collection faster and more natural — which we cover next in this series.&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="https://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
    </item>
    <item>
      <title>Capturing Human Demonstrations for Imitation Learning</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:19:33 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/capturing-human-demonstrations-for-imitation-learning-1pdd</link>
      <guid>https://dev.to/vmodal_ai/capturing-human-demonstrations-for-imitation-learning-1pdd</guid>
      <description>&lt;h1&gt;
  
  
  Capturing Human Demonstrations for Imitation Learning
&lt;/h1&gt;

&lt;p&gt;Once you have a working teleoperation rig, the next challenge is turning raw teleop sessions into a clean, well-structured demonstration dataset. This is where most imitation learning projects quietly succeed or fail — a policy trained on inconsistent or noisy demonstrations will faithfully reproduce that inconsistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as a "demonstration"
&lt;/h2&gt;

&lt;p&gt;A demonstration is a single, complete episode of a task: from a defined start state to a defined end state (success, failure, or reset), recorded as a synchronized sequence of observations and actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Demonstration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;episode_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;observations&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;joint&lt;/span&gt; &lt;span class="nx"&gt;states&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;proprioception&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commanded&lt;/span&gt; &lt;span class="nx"&gt;joint&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;effector&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gripper&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;metadata          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operator&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treating episodes as discrete, labeled units (rather than one long continuous log) is what makes the dataset usable for supervised learning later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing your data schema before you collect anything
&lt;/h2&gt;

&lt;p&gt;It's tempting to start recording immediately, but a few minutes spent on schema design saves days of reprocessing later. At minimum, decide on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observation space&lt;/strong&gt;: which camera(s), resolution, joint state format (position/velocity/torque), and any additional sensors (force-torque, tactile).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action space&lt;/strong&gt;: joint-space targets vs. end-effector pose targets vs. delta actions. This choice has a big effect on how well imitation learning generalizes — delta (relative) actions are often more robust to small state drift than absolute targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sampling rate&lt;/strong&gt;: pick one rate for the whole pipeline (commonly 10–30 Hz for manipulation) and resample everything to it rather than mixing rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File format&lt;/strong&gt;: HDF5 and per-episode directories with images-as-files plus a metadata file (e.g., a "LeRobot dataset"-style layout) are both common. HDF5 is compact and fast to load; a directory-per-episode layout is easier to inspect and debug.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recording the episode loop
&lt;/h2&gt;

&lt;p&gt;Extending the teleoperation loop from the previous tutorial, wrap it with clear episode lifecycle events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoRecorder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;teleop_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dataset_writer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;teleop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;teleop_session&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataset_writer&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_episode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_label&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;episode_meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task_label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;end_episode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;episode_meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;episode_meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_episode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;episode_meta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key practice: let the operator explicitly mark success/failure at the end of each episode rather than inferring it automatically. Automated success detection is useful later for filtering, but early on, an honest human label is more trustworthy than a heuristic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data quality practices that matter more than volume
&lt;/h2&gt;

&lt;p&gt;A smaller dataset of clean, diverse demonstrations usually outperforms a large dataset of repetitive or noisy ones. A few concrete practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vary initial conditions.&lt;/strong&gt; Randomize object position, orientation, and (if applicable) lighting between episodes. A policy trained only on one fixed starting configuration will not generalize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collect multiple operators if possible.&lt;/strong&gt; A single operator's idiosyncratic style can become an artifact the policy overfits to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discard aborted or corrected episodes&lt;/strong&gt;, or label them explicitly as "corrected" rather than silently keeping them — mixing recovery behavior into clean success trajectories confuses the policy about what "correct" execution looks like.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Balance the dataset across task variations.&lt;/strong&gt; If 80% of your demonstrations are the easy variant of a task, the policy will be biased toward it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanity-check episode length distribution.&lt;/strong&gt; Extremely short or extremely long episodes (compared to the median) usually indicate a teleop glitch or an operator mistake — review before including them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Post-hoc validation before training
&lt;/h2&gt;

&lt;p&gt;Before you ever hand this dataset to a training script, run a validation pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_episode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;episode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_hz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_shape&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;timestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;episode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:])]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;expected_hz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Frame drop detected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;episode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;image_shape&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NaN in action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checks worth automating: dropped frames, NaNs in joint states or actions, camera frames that are all-black or all-white (a common sign of a disconnected or misconfigured camera), and gripper state that never changes across an episode where it clearly should (often a sign of a logging bug, not real behavior).&lt;/p&gt;

&lt;h2&gt;
  
  
  Visualizing before trusting
&lt;/h2&gt;

&lt;p&gt;Always render a handful of random episodes back as video with overlaid action values before considering a collection session "done." This catches problems no automated check will — an operator drifting off-task, an object that rolled out of frame, or a gripper that visually never closes despite the logged command saying it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  From raw demos to a training-ready dataset
&lt;/h2&gt;

&lt;p&gt;Once you have validated, well-labeled episodes, the natural next step is assembling them into the full imitation learning pipeline: splitting into train/validation sets, normalizing observations and actions, and feeding them into a policy architecture — which is exactly what we build in the next tutorial in this series.&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="https://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
    </item>
    <item>
      <title>Building a Multi-Layer Robot Software Architecture from Sensors to AI</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:19:32 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/building-a-multi-layer-robot-software-architecture-from-sensors-to-ai-2c4a</link>
      <guid>https://dev.to/vmodal_ai/building-a-multi-layer-robot-software-architecture-from-sensors-to-ai-2c4a</guid>
      <description>&lt;h1&gt;
  
  
  Building a Multi-Layer Robot Software Architecture from Sensors to AI
&lt;/h1&gt;

&lt;p&gt;A modern robot combines hardware drivers, sensor processing, AI models, localization, planning, control, and user applications. Without clear boundaries, these components quickly become difficult to maintain.&lt;/p&gt;

&lt;p&gt;This tutorial shows how to organize a robot software stack from the physical sensor layer to AI-driven applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Seven-Layer Model
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 7 — Applications / Mission
Layer 6 — AI Reasoning
Layer 5 — Planning
Layer 4 — World Model / Localization
Layer 3 — Perception
Layer 2 — ROS 2 Middleware / Interfaces
Layer 1 — Hardware &amp;amp; Drivers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer should have a clear responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Hardware and Drivers
&lt;/h2&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RGB cameras&lt;/li&gt;
&lt;li&gt;Depth cameras&lt;/li&gt;
&lt;li&gt;LiDAR&lt;/li&gt;
&lt;li&gt;IMU&lt;/li&gt;
&lt;li&gt;GPS&lt;/li&gt;
&lt;li&gt;Wheel encoders&lt;/li&gt;
&lt;li&gt;Motor controllers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drivers translate hardware-specific protocols into standardized software interfaces.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Camera SDK
    ↓
Camera Driver
    ↓
ROS 2 Image Message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Layer 2: ROS 2 Communication
&lt;/h2&gt;

&lt;p&gt;ROS 2 nodes communicate through a graph. Topics are useful for continuous streams, services for short request/response operations, and actions for long-running operations with feedback. &lt;a href="https://docs.ros.org/en/rolling/Concepts/Basic/Interfaces-Topics-Services-Actions.html" rel="noopener noreferrer"&gt;ROS 2 documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;camera_node
     ↓
/camera/image_raw
     ↓
perception_node
     ↓
/detections
     ↓
planner_node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This decouples producers from consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Perception
&lt;/h2&gt;

&lt;p&gt;Perception converts raw sensor signals into meaningful information.&lt;/p&gt;

&lt;p&gt;Typical components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Image → Object Detection
Image → Segmentation
Depth → 3D Points
LiDAR → Obstacles
IMU → Motion Information
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A perception output could look conceptually like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"person"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;2.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not let application code depend directly on model-specific output. Define a stable perception interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: World Model and Localization
&lt;/h2&gt;

&lt;p&gt;The robot needs to combine observations into a representation of the world.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sensors
   ↓
Sensor Fusion
   ↓
Localization
   ↓
World Model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The world model may contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Robot pose&lt;/li&gt;
&lt;li&gt;Occupancy information&lt;/li&gt;
&lt;li&gt;Dynamic objects&lt;/li&gt;
&lt;li&gt;Static landmarks&lt;/li&gt;
&lt;li&gt;Navigation goals&lt;/li&gt;
&lt;li&gt;Velocity&lt;/li&gt;
&lt;li&gt;Uncertainty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This layer provides planning with structured information rather than raw sensor streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 5: Planning
&lt;/h2&gt;

&lt;p&gt;Planning answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What should the robot do next?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For navigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current Pose + Map + Goal
             ↓
          Planner
             ↓
           Path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nav2 is a ROS 2 navigation framework that provides components for autonomous navigation and is designed to work across different robot configurations. &lt;a href="https://docs.nav2.org/" rel="noopener noreferrer"&gt;Nav2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Planning can also include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Task planning&lt;/li&gt;
&lt;li&gt;Motion planning&lt;/li&gt;
&lt;li&gt;Manipulation planning&lt;/li&gt;
&lt;li&gt;Behavior trees&lt;/li&gt;
&lt;li&gt;Recovery behaviors&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Layer 6: AI Reasoning
&lt;/h2&gt;

&lt;p&gt;AI can operate above traditional robotics algorithms.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: "Bring the box from the storage room."

AI Reasoner
    ↓
Task decomposition
    ↓
Navigate → Find box → Pick → Return
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI should produce structured intentions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"task"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"navigate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"storage_room"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A deterministic task executor then validates and executes that intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 7: Application and Mission Layer
&lt;/h2&gt;

&lt;p&gt;The highest layer manages user-facing workflows.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warehouse missions&lt;/li&gt;
&lt;li&gt;Delivery tasks&lt;/li&gt;
&lt;li&gt;Inspection workflows&lt;/li&gt;
&lt;li&gt;Human-robot interaction&lt;/li&gt;
&lt;li&gt;Fleet management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A mobile dashboard could visualize:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Robot
├── Position
├── Battery
├── Mission
├── Camera
├── Detected Objects
└── Health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This layer should not contain low-level motor control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define Interfaces Between Layers
&lt;/h2&gt;

&lt;p&gt;A useful rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer N should know the interface of Layer N-1,
not its implementation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Planner → Localization API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Planner → Specific GPS Driver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes components replaceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Repository Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;robot_stack/
├── drivers/
├── interfaces/
├── perception/
├── localization/
├── world_model/
├── planning/
├── control/
├── mission/
├── ai/
├── monitoring/
└── deployment/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing Strategy
&lt;/h2&gt;

&lt;p&gt;Test every layer independently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Driver Tests
     ↓
Interface Tests
     ↓
Perception Tests
     ↓
Planning Tests
     ↓
Control Tests
     ↓
Integration Tests
     ↓
Simulation
     ↓
Hardware
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For AI components, maintain recorded sensor datasets so model changes can be evaluated against identical inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;Measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sensor-to-perception latency&lt;/li&gt;
&lt;li&gt;Perception-to-planning latency&lt;/li&gt;
&lt;li&gt;Planning duration&lt;/li&gt;
&lt;li&gt;Control-loop jitter&lt;/li&gt;
&lt;li&gt;Message frequency&lt;/li&gt;
&lt;li&gt;CPU/GPU utilization&lt;/li&gt;
&lt;li&gt;Memory usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A robot can have highly accurate AI and still fail if decisions arrive too late.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Layering a robot software stack creates clear boundaries between hardware, communication, perception, planning, AI, and applications.&lt;/p&gt;

&lt;p&gt;The most valuable benefit is replaceability. You can change a camera, AI model, planner, or user interface without rewriting the entire robot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="http://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;https://github.com/v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;https://github.com/v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building a Robot Teleoperation System for Data Collection</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:19:29 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/building-a-robot-teleoperation-system-for-data-collection-3e33</link>
      <guid>https://dev.to/vmodal_ai/building-a-robot-teleoperation-system-for-data-collection-3e33</guid>
      <description>&lt;h1&gt;
  
  
  Building a Robot Teleoperation System for Data Collection
&lt;/h1&gt;

&lt;p&gt;If you're training robot policies with imitation learning, the quality of your dataset almost entirely depends on how good your teleoperation system is. A clunky, high-latency teleop rig produces jerky, inconsistent demonstrations — and your policy will happily learn those bad habits. This tutorial walks through designing and building a teleoperation system purpose-built for collecting clean, high-frequency demonstration data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "good" teleoperation looks like for data collection
&lt;/h2&gt;

&lt;p&gt;Teleoperation for entertainment or remote inspection has different priorities than teleoperation for data collection. For ML data collection, you specifically need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low, consistent latency&lt;/strong&gt; — variable lag introduces noise that looks like intentional motion to a learning algorithm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-frequency, synchronized logging&lt;/strong&gt; — every joint command needs a timestamp that lines up with camera frames and sensor readings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smooth, continuous control&lt;/strong&gt; — discrete or bang-bang inputs (keyboard-style) produce trajectories that don't resemble natural human motion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatability&lt;/strong&gt; — the same operator should be able to produce similar trajectories across many trials, which matters for later behavior cloning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  System architecture
&lt;/h2&gt;

&lt;p&gt;A typical teleoperation-for-data-collection stack has four layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Input device layer&lt;/strong&gt; — leader arm, joystick, VR controller, or a haptic device that produces a continuous control signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mapping layer&lt;/strong&gt; — converts the input device's pose/state into a target pose or joint command for the follower robot (this is where retargeting happens if the input and robot have different kinematics).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control layer&lt;/strong&gt; — a low-level controller (impedance, PD, or inverse kinematics solver) that turns target commands into actuator signals safely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging layer&lt;/strong&gt; — records synchronized streams: joint states, end-effector pose, camera frames, gripper state, and the raw input signal.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Input Device] -&amp;gt; [Mapping/Retargeting] -&amp;gt; [Robot Controller] -&amp;gt; [Actuators]
                                                    |
                                                    v
                                          [Synchronized Logger]
                                          (joints, images, timestamps)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Choosing your input device
&lt;/h2&gt;

&lt;p&gt;Common choices, roughly in order of setup complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leader-follower arm pairs&lt;/strong&gt; (e.g., a low-cost duplicate arm you move by hand) — gives the most natural kinesthetic feel and near 1:1 kinematic mapping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3D mouse / SpaceMouse&lt;/strong&gt; — cheap, precise for 6-DOF end-effector control, but less intuitive for beginners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gamepad&lt;/strong&gt; — good for mobile base + simple arm tasks, poor for fine manipulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VR controllers&lt;/strong&gt; — excellent for full 6-DOF pose control with intuitive hand tracking (covered in depth in a separate tutorial in this series).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For manipulation tasks specifically, leader-follower arms and VR controllers tend to produce the cleanest demonstrations because the operator's hand motion maps almost directly onto the desired end-effector motion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing the control loop
&lt;/h2&gt;

&lt;p&gt;Here's a minimal Python control loop structure using an end-effector pose target and a simple IK-based controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TeleopSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;control_hz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_device&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;robot&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;control_hz&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duration_s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;duration_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;loop_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="nb"&gt;raw_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;target_pose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gripper_cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;raw_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;joint_cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inverse_kinematics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_pose&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_joint_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;joint_cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gripper_cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;loop_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_pose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;target_pose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;joint_state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_joint_state&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gripper_state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gripper_cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_camera_frame&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;

            &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;loop_start&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;map_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;raw_input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Retargeting logic goes here: scale, filter, clamp
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;NotImplementedError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few practical notes on this loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run it at a fixed control rate (20–50 Hz is typical) and log the &lt;em&gt;actual&lt;/em&gt; elapsed time per iteration, not just the nominal timestamp — drift matters for imitation learning.&lt;/li&gt;
&lt;li&gt;Apply a low-pass filter to the raw input signal before mapping it to a target pose. Human hand tremor and sensor noise both show up as high-frequency jitter that hurts downstream policy training.&lt;/li&gt;
&lt;li&gt;Always clamp the target pose to safe workspace bounds &lt;em&gt;before&lt;/em&gt; sending it to the IK solver, not after — this avoids solver instability near joint limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Synchronizing multiple data streams
&lt;/h2&gt;

&lt;p&gt;The hardest part of a teleop data collection system usually isn't the control loop — it's keeping the camera stream, joint state stream, and input stream synchronized. A few approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single-threaded polling loop&lt;/strong&gt; (shown above) — simplest, works well if your camera and robot APIs are fast enough to poll synchronously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-threaded with timestamp alignment&lt;/strong&gt; — each sensor runs on its own thread/process and pushes timestamped samples into a shared buffer; a separate aligner thread matches samples by nearest timestamp. Necessary once you add multiple cameras or higher-rate sensors like force-torque.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware trigger sync&lt;/strong&gt; — for research-grade setups, a hardware trigger line pulses all sensors simultaneously. Overkill for most hobbyist or startup projects, but worth knowing about if you're chasing sub-millisecond alignment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most imitation learning use cases, multi-threaded polling with timestamp-based nearest-neighbor alignment is a good default — it's robust and doesn't require special hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safety layers you shouldn't skip
&lt;/h2&gt;

&lt;p&gt;Because a human is now directly driving a robot arm in real time, add these guardrails regardless of how "just for data collection" the setup feels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workspace bounding box enforced in software, independent of the IK solver.&lt;/li&gt;
&lt;li&gt;A velocity limiter on the mapped target pose to prevent sudden jumps if the input device glitches.&lt;/li&gt;
&lt;li&gt;A dead-man's switch (physical button or trigger) that must be held to enable motion — releasing it should freeze or gently stop the robot.&lt;/li&gt;
&lt;li&gt;An emergency stop that cuts power at the hardware level, not just a software flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;Once your loop is running reliably, wrap each demonstration collection session with clear episode boundaries — a "start recording" and "end recording" signal (often a button press) so each demonstration becomes a discrete, labeled trajectory rather than one continuous, ambiguous stream. This episodic structure is exactly what you'll need in the next step of the pipeline: capturing human demonstrations for imitation learning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="https://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
    </item>
    <item>
      <title>Building Synthetic Training Data Pipelines for Robotics</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:19:18 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/building-synthetic-training-data-pipelines-for-robotics-542n</link>
      <guid>https://dev.to/vmodal_ai/building-synthetic-training-data-pipelines-for-robotics-542n</guid>
      <description>&lt;h1&gt;
  
  
  Building Synthetic Training Data Pipelines for Robotics
&lt;/h1&gt;

&lt;p&gt;Real-world robot data is expensive to collect and label. Synthetic data pipelines — generating training data directly from simulation, complete with automatic ground-truth labels — let you produce far larger and more diverse datasets than manual collection ever could, for a fraction of the cost. This tutorial covers designing and building a synthetic data pipeline for robotics, from scene generation through to a usable, labeled dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where synthetic data fits in a robot learning pipeline
&lt;/h2&gt;

&lt;p&gt;Synthetic data pipelines are most valuable for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Perception training&lt;/strong&gt; — object detection, segmentation, pose estimation, and grasp point prediction all benefit enormously from large volumes of perfectly labeled data, which is exactly what simulation provides for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrapping policies before real demonstrations exist&lt;/strong&gt; — pretraining on synthetic rollouts, then fine-tuning on a smaller set of real demonstrations (the same pattern discussed in the sim-to-real tutorial).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rare or dangerous scenarios&lt;/strong&gt; — edge cases that are hard or unsafe to reproduce repeatedly on real hardware (near-collisions, extreme object configurations, sensor failure modes) are trivial to generate synthetically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a full replacement for real data — visual and dynamics gaps mean synthetic-only training rarely matches real-data performance for the hardest tasks — but as a large, cheap, well-labeled complement to a smaller real dataset, it's extremely effective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipeline architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scene Generation -&amp;gt; Domain Randomization -&amp;gt; Rendering/Simulation -&amp;gt; Ground-Truth Extraction -&amp;gt; Dataset Export
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage is a distinct, testable component, which matters because synthetic pipelines tend to run at large scale (thousands to millions of samples) — a bug caught late costs far more compute to regenerate than one caught early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Scene generation
&lt;/h2&gt;

&lt;p&gt;Programmatically assemble scenes rather than hand-building each one. A scene generator typically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Samples a task-relevant object (or set of objects) from an asset library.&lt;/li&gt;
&lt;li&gt;Places it at a randomized, but physically valid, pose (checking for collisions/overlaps).&lt;/li&gt;
&lt;li&gt;Samples a background/environment configuration.&lt;/li&gt;
&lt;li&gt;Places the robot and camera(s) according to the deployment configuration (with some randomization, per the domain randomization tutorial).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset_library&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset_library&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sample_valid_pose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;background&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BACKGROUND_LIBRARY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object_pose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;background&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sample_valid_pose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_attempts&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WORKSPACE_MIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WORKSPACE_MAX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;check_collision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pose&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pose&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to sample a valid pose&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Domain randomization as a data augmentation strategy
&lt;/h2&gt;

&lt;p&gt;For synthetic data generation specifically, domain randomization does double duty: it improves sim-to-real transfer (as covered previously) &lt;em&gt;and&lt;/em&gt; it's your primary mechanism for dataset diversity, replacing the manual scene variation a human data collector would otherwise need to provide. Apply the same categories covered in the domain randomization tutorial — lighting, textures, camera pose, distractors — but here, treat "diversity of the generated dataset" as the explicit success metric, not just "policy robustness."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Rendering and extracting ground truth
&lt;/h2&gt;

&lt;p&gt;This is where synthetic pipelines earn their value: ground-truth labels that would require expensive manual annotation in the real world come for free from the simulator's internal state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_and_label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rgb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_rgb&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_depth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;segmentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_instance_segmentation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;bbox_2d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_2d_bbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segmentation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pose_6d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object_pose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ground-truth 6-DOF pose
&lt;/span&gt;    &lt;span class="n"&gt;grasp_points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_grasp_candidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rgb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;depth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segmentation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;segmentation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bbox_2d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bbox_2d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pose_6d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pose_6d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grasp_points&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grasp_points&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common label types worth extracting depending on your downstream task: 2D/3D bounding boxes, instance/semantic segmentation masks, 6-DOF object pose, depth maps, surface normals, keypoints, and grasp candidate annotations. Since these all come from querying the simulator's internal state rather than from a human annotator, generating additional label types later costs essentially nothing compared to going back and re-annotating a real dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Dataset export
&lt;/h2&gt;

&lt;p&gt;Export to a format your training pipeline (and ideally, standard tooling) can consume directly. Common choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;COCO format&lt;/strong&gt; for detection/segmentation tasks — widely supported by existing training frameworks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A custom schema mirroring your real-data format&lt;/strong&gt; — often the better choice, since it lets you mix synthetic and real samples in the same training pipeline without a translation layer.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;export_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;image_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/images/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sample_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;save_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rgb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;annotation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bbox_2d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bbox_2d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pose_6d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pose_6d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grasp_points&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grasp_points&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/annotations/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sample_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;annotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Scaling generation
&lt;/h2&gt;

&lt;p&gt;Because each sample is independent, synthetic data generation parallelizes naturally across processes or machines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pool&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_one_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;
    &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_scene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ASSET_LIBRARY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setup_simulation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_and_label&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;export_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_samples&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_workers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generate_one_sample&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At real scale (hundreds of thousands of samples), this typically moves to a distributed job queue (e.g., across a cluster or cloud batch jobs) rather than a single machine's multiprocessing pool, but the underlying per-sample independence is what makes that scaling straightforward in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating the synthetic dataset
&lt;/h2&gt;

&lt;p&gt;Synthetic data pipelines fail silently more often than real data collection does — a bug in your labeling code produces plausible-looking but wrong labels, and nothing about the pipeline will complain. Build validation in from the start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visualize a random sample of generated images with labels overlaid&lt;/strong&gt; (bounding boxes, segmentation masks, projected 6-DOF pose axes) every time you change the pipeline, not just once at the beginning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check label statistics&lt;/strong&gt; — distribution of object poses, bounding box sizes, and class balance — for unexpected clustering that suggests a sampling bug rather than genuine diversity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Train a quick baseline model on a small synthetic subset&lt;/strong&gt; and sanity-check its behavior before committing to generating the full dataset at scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mixing synthetic and real data
&lt;/h2&gt;

&lt;p&gt;The most effective real-world pipelines rarely use synthetic data alone. A common and effective pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a large, diverse synthetic dataset covering broad variation.&lt;/li&gt;
&lt;li&gt;Collect a smaller real dataset (using the teleoperation and demonstration capture pipelines covered earlier in this series) covering the specific deployment conditions.&lt;/li&gt;
&lt;li&gt;Either pretrain on synthetic data and fine-tune on real data, or mix both into a single training set with real data oversampled relative to its natural proportion, since it's typically more valuable per-sample for closing the sim-to-real gap.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Closing the loop
&lt;/h2&gt;

&lt;p&gt;This tutorial completes the simulation side of the pipeline: Isaac Sim and MuJoCo for building and testing policies, sim-to-real and domain randomization techniques for making those policies transfer, and synthetic data generation for scaling training data cheaply. Combined with the teleoperation, demonstration capture, and imitation learning tutorials earlier in this series, you now have both the real and synthetic halves of a complete robot learning pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="https://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>ai</category>
      <category>simulation</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Designing a Production-Ready Physical AI Architecture</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:18:56 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/designing-a-production-ready-physical-ai-architecture-50bf</link>
      <guid>https://dev.to/vmodal_ai/designing-a-production-ready-physical-ai-architecture-50bf</guid>
      <description>&lt;h1&gt;
  
  
  Designing a Production-Ready Physical AI Architecture
&lt;/h1&gt;

&lt;p&gt;Moving a robotics prototype into production requires much more than improving model accuracy. A production Physical AI system must handle unreliable sensors, changing environments, timing constraints, hardware failures, software updates, observability, and safety.&lt;/p&gt;

&lt;p&gt;This tutorial presents a practical architecture for building robust Physical AI applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture at a Glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Mission / UX
                         │
                         ▼
                Behavior Orchestration
                         │
              ┌──────────┴──────────┐
              ▼                     ▼
         AI Reasoning           Planning
              │                     │
              └──────────┬──────────┘
                         ▼
                 World / Robot State
                         │
              ┌──────────┴──────────┐
              ▼                     ▼
         Perception             Localization
              │                     │
              └──────────┬──────────┘
                         ▼
                    ROS 2 Graph
                         │
              ┌──────────┴──────────┐
              ▼                     ▼
           Sensors              Actuators
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ROS 2 organizes distributed computation as a graph of nodes and supports communication through topics, services, and actions. &lt;a href="https://docs.ros.org/en/rolling/Concepts/Basic.html" rel="noopener noreferrer"&gt;ROS 2 concepts&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Separate Fast and Slow Loops
&lt;/h2&gt;

&lt;p&gt;Not every component should run at the same frequency.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Motor Control       100–1000 Hz
State Estimation     50–200 Hz
Perception           10–60 Hz
Planning             1–20 Hz
Mission Reasoning    Event-driven
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exact frequencies depend on the robot. The architectural principle is to avoid allowing a slow AI operation to block a safety-critical control loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Establish a Hardware Abstraction Layer
&lt;/h2&gt;

&lt;p&gt;Hardware drivers should expose stable interfaces.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application → Motor SDK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    ↓
Robot Interface
    ↓
Hardware Adapter
    ↓
Motor SDK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you replace a motor controller without changing mission logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Build a Reliable State Layer
&lt;/h2&gt;

&lt;p&gt;A robot needs a consistent state representation.&lt;/p&gt;

&lt;p&gt;Include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RobotState
├── timestamp
├── pose
├── velocity
├── battery
├── localization_status
├── safety_status
├── active_mission
└── faults[]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All state should be timestamped so downstream components can reason about freshness.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat AI as a Component, Not the Whole System
&lt;/h2&gt;

&lt;p&gt;A common mistake is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM / Vision Model → Robot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safer architecture is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Model
   ↓
Intent / Proposal
   ↓
Validation
   ↓
Planner
   ↓
Safety Layer
   ↓
Controller
   ↓
Robot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI should propose actions while deterministic software enforces constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Design Explicit Safety Boundaries
&lt;/h2&gt;

&lt;p&gt;Safety should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maximum velocity&lt;/li&gt;
&lt;li&gt;Maximum acceleration&lt;/li&gt;
&lt;li&gt;Workspace limits&lt;/li&gt;
&lt;li&gt;Collision constraints&lt;/li&gt;
&lt;li&gt;Emergency stop&lt;/li&gt;
&lt;li&gt;Watchdogs&lt;/li&gt;
&lt;li&gt;Sensor health checks&lt;/li&gt;
&lt;li&gt;Battery limits&lt;/li&gt;
&lt;li&gt;Communication timeouts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_velocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wz&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_LINEAR_SPEED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MAX_ANGULAR_SPEED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never assume an upstream AI component will always produce valid output.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Use ROS 2 Interfaces Intentionally
&lt;/h2&gt;

&lt;p&gt;Use &lt;strong&gt;topics&lt;/strong&gt; for continuous data such as sensor streams and robot state.&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;services&lt;/strong&gt; for short request/response operations.&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;actions&lt;/strong&gt; for long-running robot behaviors that require feedback or cancellation. &lt;a href="https://docs.ros.org/en/lyrical/How-To-Guides/Topics-Services-Actions.html" rel="noopener noreferrer"&gt;ROS 2 interface guidance&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This semantic separation makes a distributed system easier to understand and debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Add Fault Handling
&lt;/h2&gt;

&lt;p&gt;Every important subsystem should expose health information.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Camera
  └── HEALTHY

LiDAR
  └── DEGRADED

Localization
  └── HEALTHY

Motor Controller
  └── FAULT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mission manager can then decide whether to continue, retry, stop, or request human intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Design for Graceful Degradation
&lt;/h2&gt;

&lt;p&gt;Suppose the RGB camera fails.&lt;/p&gt;

&lt;p&gt;A robust robot might transition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Full Perception
      ↓ camera failure
Reduced Perception
      ↓
Safe Navigation
      ↓
Return / Stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid architectures where one optional sensor failure crashes the entire autonomy stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Make Deployment Reproducible
&lt;/h2&gt;

&lt;p&gt;Use containers where appropriate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker
├── perception
├── planning
├── monitoring
└── application
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Models&lt;/li&gt;
&lt;li&gt;Configuration&lt;/li&gt;
&lt;li&gt;ROS packages&lt;/li&gt;
&lt;li&gt;Dependencies&lt;/li&gt;
&lt;li&gt;Hardware firmware&lt;/li&gt;
&lt;li&gt;Calibration files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A robot should be reproducible from a known software release.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Observability
&lt;/h2&gt;

&lt;p&gt;Production telemetry should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What was the robot doing?&lt;/li&gt;
&lt;li&gt;What did it perceive?&lt;/li&gt;
&lt;li&gt;What did the planner decide?&lt;/li&gt;
&lt;li&gt;Why did the controller stop?&lt;/li&gt;
&lt;li&gt;How long did inference take?&lt;/li&gt;
&lt;li&gt;Which sensor was unavailable?&lt;/li&gt;
&lt;li&gt;What safety rule triggered?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store structured events rather than relying only on console output.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Security
&lt;/h2&gt;

&lt;p&gt;Networked robots should authenticate components and restrict interfaces.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Device Identity
      ↓
Authentication
      ↓
Authorization
      ↓
Encrypted Communication
      ↓
ROS 2 / Application Services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply least privilege to remote control, diagnostics, and software updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Validate with Failure Scenarios
&lt;/h2&gt;

&lt;p&gt;Create explicit tests for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Camera disconnect&lt;/li&gt;
&lt;li&gt;LiDAR timeout&lt;/li&gt;
&lt;li&gt;Localization loss&lt;/li&gt;
&lt;li&gt;Network interruption&lt;/li&gt;
&lt;li&gt;GPU failure&lt;/li&gt;
&lt;li&gt;Low battery&lt;/li&gt;
&lt;li&gt;Stuck actuator&lt;/li&gt;
&lt;li&gt;Invalid AI output&lt;/li&gt;
&lt;li&gt;Planner timeout&lt;/li&gt;
&lt;li&gt;Emergency stop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Production readiness comes from predictable behavior under failure, not just success-case demonstrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A production Physical AI architecture should be layered, observable, fault-tolerant, and safety-oriented. AI can provide powerful perception and reasoning, but deterministic components should remain responsible for validation, constraints, control, and emergency behavior.&lt;/p&gt;

&lt;p&gt;The goal is not to create a robot that works only when everything is perfect. The goal is to create one that behaves predictably when reality is imperfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="http://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;https://github.com/v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;https://github.com/v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Domain Randomization for Robust Robot Learning</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:18:49 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/domain-randomization-for-robust-robot-learning-396b</link>
      <guid>https://dev.to/vmodal_ai/domain-randomization-for-robust-robot-learning-396b</guid>
      <description>&lt;h1&gt;
  
  
  Domain Randomization for Robust Robot Learning
&lt;/h1&gt;

&lt;p&gt;Domain randomization is one of the most effective and widely used techniques for training robot policies in simulation that actually work on real hardware. Instead of trying to make simulation match reality as precisely as possible, domain randomization deliberately varies simulation parameters across a wide range during training, so the policy learns to be robust to variation rather than overfit to one specific (and inevitably slightly wrong) simulated world. This tutorial covers what to randomize, how much, and how to structure training around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;If a policy is trained across thousands of simulated variations — different friction coefficients, different lighting, different object textures, different camera positions — the real world simply becomes one more sample from that broad training distribution, rather than an unfamiliar edge case the policy has never encountered. The policy learns to rely on features and strategies that are robust across the whole distribution, rather than any single simulation's specific quirks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to randomize: dynamics parameters
&lt;/h2&gt;

&lt;p&gt;For contact-rich and physically demanding tasks, randomizing the physics parameters that most affect real-world dynamics mismatch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Friction coefficients&lt;/strong&gt; (ground, object surfaces, gripper pads)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mass and inertia&lt;/strong&gt; of manipulated objects and robot links&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Joint damping and friction&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actuator strength/gain&lt;/strong&gt; and torque limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contact stiffness/softness parameters&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;randomize_dynamics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nbody&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body_mass&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;njnt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dof_damping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ngeom&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;geom_friction&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A practical starting point is randomizing by a percentage range (e.g., ±20–30%) around your best system-identified estimate, rather than guessing an absolute range — this keeps the distribution centered on plausible reality instead of centered on an arbitrary default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to randomize: visual parameters
&lt;/h2&gt;

&lt;p&gt;For vision-based policies, visual domain randomization is often the highest-leverage category, since visual mismatch tends to dominate the sim-to-real gap for camera-driven tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lighting&lt;/strong&gt; — position, intensity, color temperature, number of light sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Textures&lt;/strong&gt; — randomize materials on the ground, background, table, and even the robot itself, sampling from large texture datasets rather than a handful of hand-picked options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera parameters&lt;/strong&gt; — position, orientation, field of view, and even slight lens distortion, within a range consistent with real mounting tolerances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distractor objects&lt;/strong&gt; — randomly placed irrelevant objects in the scene so the policy learns to attend to task-relevant objects specifically, rather than memorizing "the third object in the scene."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color/appearance of the target object&lt;/strong&gt; — if applicable, so the policy generalizes to object appearance variation rather than one hardcoded color/texture.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;randomize_visuals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;light_intensity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;light_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ground_texture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEXTURE_LIBRARY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;camera_position&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;camera_fov&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What to randomize: control and sensing
&lt;/h2&gt;

&lt;p&gt;Beyond physics and visuals, randomizing aspects of the control loop itself helps close the latency and noise gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Observation noise&lt;/strong&gt; — add Gaussian noise to joint position/velocity readings and camera images to mimic real sensor imperfection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action latency&lt;/strong&gt; — randomly delay applied actions by a few control steps to simulate real communication and processing latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control frequency jitter&lt;/strong&gt; — occasionally skip or double a control step to mimic the timing irregularities of a real control loop under load.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_sensor_noise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noise_std&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;obs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noise_std&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How much randomization is too much
&lt;/h2&gt;

&lt;p&gt;There's a real tradeoff here: too little randomization and the policy remains brittle to the exact gap sources it wasn't exposed to; too much randomization and the task becomes so variable that the policy struggles to learn &lt;em&gt;anything&lt;/em&gt; useful, or learns an overly conservative, low-performance strategy just to survive the worst-case samples.&lt;/p&gt;

&lt;p&gt;Practical guidance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start narrow, then widen.&lt;/strong&gt; Begin with a modest randomization range validated against your system-identified real-world parameters, confirm the policy still learns the task well in simulation, then progressively widen the range while monitoring simulated task performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Randomize what plausibly varies in the real deployment, not everything imaginable.&lt;/strong&gt; If your robot always operates under consistent lighting, heavy lighting randomization mostly wastes training capacity; if it operates in varied environments, it's essential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track both simulated success rate and (periodically) real-world success rate&lt;/strong&gt; as you widen randomization ranges — the goal is the real-world number going up, not the simulated one going down as little as possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automatic domain randomization (ADR)
&lt;/h2&gt;

&lt;p&gt;A more advanced approach automatically adjusts randomization ranges during training based on policy performance, rather than using fixed ranges chosen up front:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with a narrow randomization range.&lt;/li&gt;
&lt;li&gt;Periodically evaluate the policy at the edges of the current range.&lt;/li&gt;
&lt;li&gt;If performance at the edges is still good, widen the range slightly; if performance degrades, hold or narrow it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This produces a curriculum that automatically scales randomization difficulty to match the policy's current capability, rather than requiring the practitioner to guess the right fixed range in advance — but it adds meaningful implementation complexity and is usually only worth it once you've validated the basic fixed-range approach works for your task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structuring the training loop
&lt;/h2&gt;

&lt;p&gt;A minimal per-episode randomization hook, integrated into an RL or imitation-learning-with-simulated-rollout training loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reset_randomized_episode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;randomization_config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;randomize_dynamics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;randomize_visuals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;obs&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;episode&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_episodes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reset_randomized_episode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;randomization_config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;maybe_delay_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# simulate control latency
&lt;/span&gt;        &lt;span class="n"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reward&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;obs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;add_sensor_noise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Randomizing &lt;em&gt;every&lt;/em&gt; episode (not just occasionally) is important — if only a fraction of training episodes are randomized, the policy can still find shortcuts that work for the common, non-randomized case and fail exactly where robustness matters most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring whether it's working
&lt;/h2&gt;

&lt;p&gt;The real test of domain randomization isn't simulated performance — it's whether real-world performance improves, and ideally with less real-world fine-tuning data needed than a policy trained without randomization. Track a fixed real-world evaluation suite (as discussed in the sim-to-real tutorial) before and after adding or widening randomization, and treat any randomization change that doesn't measurably help real-world performance as a hyperparameter not worth keeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  From randomized simulation to structured datasets
&lt;/h2&gt;

&lt;p&gt;Domain randomization is also the foundation of another major use case: generating large-scale synthetic training datasets with rich, automatically-labeled ground truth — which is exactly what we build in the final tutorial of this series.&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="https://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>ai</category>
      <category>simulation</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Sim-to-Real Transfer for Physical AI Robots</title>
      <dc:creator>vmodal_ai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:18:44 +0000</pubDate>
      <link>https://dev.to/vmodal_ai/sim-to-real-transfer-for-physical-ai-robots-11mb</link>
      <guid>https://dev.to/vmodal_ai/sim-to-real-transfer-for-physical-ai-robots-11mb</guid>
      <description>&lt;h1&gt;
  
  
  Sim-to-Real Transfer for Physical AI Robots
&lt;/h1&gt;

&lt;p&gt;Training a policy in simulation is fast, safe, and scalable — but it's only useful if that policy also works on the real robot. The gap between simulated and real-world performance, known as the "sim-to-real gap," is one of the central challenges in Physical AI. This tutorial covers where that gap comes from and the concrete techniques used to close it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the sim-to-real gap comes from
&lt;/h2&gt;

&lt;p&gt;The gap typically breaks down into a few distinct sources, and it's worth diagnosing which one is dominant before picking a fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dynamics mismatch&lt;/strong&gt; — simulated friction, mass, damping, and actuator response never perfectly match the real robot. Contact-rich tasks (grasping, insertion, legged locomotion) are especially sensitive to this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual mismatch&lt;/strong&gt; — simulated camera images differ from real ones in lighting, texture detail, sensor noise, and lens distortion. This matters enormously for vision-based policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency and control-loop mismatch&lt;/strong&gt; — real robots have communication delays, control loop jitter, and actuator lag that a simulated environment with perfect, instantaneous control doesn't capture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensor noise and calibration&lt;/strong&gt; — real sensors are noisier and imperfectly calibrated compared to their simulated, ground-truth counterparts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unmodeled phenomena&lt;/strong&gt; — cable drag, backlash, thermal effects, and wear are rarely modeled in simulation at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategy 1: System identification
&lt;/h2&gt;

&lt;p&gt;Before trying to make a policy "robust" to the gap, it's often worth simply making the simulation more accurate. System identification means measuring real robot parameters (joint friction, motor torque constants, link masses) and feeding them back into your simulation model.&lt;/p&gt;

&lt;p&gt;A simple approach for a single parameter (e.g., joint friction):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy.optimize&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;minimize&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;simulate_trajectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;friction_coef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initial_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Run the sim with a given friction coefficient and return resulting trajectory
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;real_trajectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initial_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;friction_coef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;sim_trajectory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;simulate_trajectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;friction_coef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initial_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sim_trajectory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real_trajectory&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;minimize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;loss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;real_trajectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initial_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nelder-Mead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fitted_friction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea generalizes: run the same command sequence on both the real robot and the simulator, then optimize simulation parameters to minimize the discrepancy between simulated and real trajectories. This is worth doing for a handful of high-leverage parameters (joint friction/damping, key link masses, actuator gain) rather than trying to identify everything at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 2: Domain randomization
&lt;/h2&gt;

&lt;p&gt;Rather than trying to match simulation to reality exactly, domain randomization deliberately trains the policy across a wide &lt;em&gt;distribution&lt;/em&gt; of simulated conditions, so the real world just looks like "one more sample" from that distribution. This is powerful enough that it gets its own dedicated tutorial next in this series — but the core idea is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Randomize dynamics parameters (friction, mass, damping, motor strength) within a plausible range every episode.&lt;/li&gt;
&lt;li&gt;Randomize visual parameters (lighting, textures, camera pose/intrinsics) for vision-based policies.&lt;/li&gt;
&lt;li&gt;Randomize latency and add simulated actuator noise to reduce the policy's reliance on perfect, instantaneous control.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategy 3: Reducing reliance on precise dynamics
&lt;/h2&gt;

&lt;p&gt;Some policy and control choices are inherently more robust to dynamics mismatch than others:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Position/impedance control over pure torque control&lt;/strong&gt; — a well-tuned impedance controller absorbs some dynamics mismatch mechanically, before it ever reaches the learned policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closed-loop, reactive policies over open-loop trajectory replay&lt;/strong&gt; — a policy that continuously observes and reacts to the current state degrades more gracefully under mismatch than one that blindly executes a pre-planned trajectory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action chunking with re-planning&lt;/strong&gt; (predict a short sequence of actions, execute a few, then re-observe and re-plan) strikes a middle ground — smoother than pure reactive single-step control, but still able to correct for accumulated error periodically.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategy 4: Closing the visual gap
&lt;/h2&gt;

&lt;p&gt;For vision-based policies specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain randomization of visual appearance&lt;/strong&gt; — randomize textures, lighting, and distractor objects during simulated training so the policy doesn't overfit to simulation-specific visual artifacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain adaptation / image translation&lt;/strong&gt; — train a model (e.g., a CycleGAN-style translator) to map real camera images into the visual style of simulation (or vice versa) before feeding them to the policy, reducing the visual distribution shift the policy has to handle natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using real background/texture datasets as randomization sources&lt;/strong&gt; — rather than purely synthetic textures, sampling from real-world image datasets during randomization tends to produce policies that generalize better to real cameras.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matching camera intrinsics and mounting position precisely&lt;/strong&gt; between simulation and the real robot — this sounds mundane but is one of the most common, easily fixed sources of vision-based sim-to-real failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategy 5: Fine-tuning on real data
&lt;/h2&gt;

&lt;p&gt;Rather than relying entirely on zero-shot sim-to-real transfer, many practical pipelines use simulation to get most of the way there, then fine-tune on a small amount of real-world data:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pretrain the policy primarily on simulated (and possibly domain-randomized) data.&lt;/li&gt;
&lt;li&gt;Collect a modest set of real demonstrations or real rollout data (often far smaller than what would be needed to train from scratch).&lt;/li&gt;
&lt;li&gt;Fine-tune the pretrained policy on this real data, ideally with a lower learning rate to avoid catastrophically forgetting the broad simulated experience.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This "sim pretraining + real fine-tuning" pattern often needs an order of magnitude less real-world data than training purely from real demonstrations, while still closing most of the residual gap that domain randomization alone doesn't fully address.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluating sim-to-real transfer honestly
&lt;/h2&gt;

&lt;p&gt;A few practices for measuring whether your transfer strategy is actually working, rather than just hoping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Track a fixed real-world evaluation suite&lt;/strong&gt; — the same set of tasks and initial conditions, run on the real robot every time you update your simulated training pipeline, so you can measure whether changes actually help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare real vs. simulated performance on matched tasks&lt;/strong&gt;, not just in isolation — a large gap that isn't shrinking over iterations is a signal that your randomization ranges or system identification may be systematically off, not just noisy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log failure modes qualitatively&lt;/strong&gt;, not just success rate. A policy that fails safely and predictably (e.g., gets close but stops) is in a very different state than one that fails chaotically (e.g., slams into the workspace boundary) — success rate alone won't tell you which one you have.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where domain randomization fits next
&lt;/h2&gt;

&lt;p&gt;Domain randomization is powerful enough, and has enough of its own design decisions (what to randomize, how much, and how to structure training around it), that it deserves its own deep dive — which is exactly the next tutorial in this series.&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;p&gt;Website: &lt;a href="https://www.v-modal.com" rel="noopener noreferrer"&gt;www.v-modal.com&lt;/a&gt;&lt;br&gt;
SDK Flutter: &lt;a href="https://github.com/v-modal/vmodal_sdk_flutter" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_flutter&lt;/a&gt;&lt;br&gt;
SDK Android: &lt;a href="https://github.com/v-modal/vmodal_sdk_android" rel="noopener noreferrer"&gt;v-modal/vmodal_sdk_android&lt;/a&gt;&lt;br&gt;
Discord: &lt;a href="https://discord.gg/K72z28KUx" rel="noopener noreferrer"&gt;https://discord.gg/K72z28KUx&lt;/a&gt;&lt;/p&gt;

</description>
      <category>robotics</category>
      <category>ai</category>
      <category>simulation</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
