gpue commited on
Commit
64f0005
·
1 Parent(s): 52e6c73

Enhance UR5e robot control with orientation tracking and UI updates

Browse files

- Added support for end-effector orientation control in the UR5e robot, allowing for 6-DOF inverse kinematics.
- Implemented new WebSocket messages for setting target orientation and toggling orientation usage.
- Updated mujoco_server.py to broadcast end-effector orientation and target orientation in state messages.
- Enhanced the UI to include orientation sliders and display for end-effector orientation.
- Refined README.md to document new features, including orientation control and updated JSON message structures.

README.md CHANGED
@@ -20,9 +20,10 @@ A unified MuJoCo-based robot simulation platform with web interface for multiple
20
  - 6 degrees of freedom (6-axis industrial arm)
21
  - Robotiq 2F-85 gripper
22
  - Two control modes:
23
- - **IK Mode**: Set target XYZ position, inverse kinematics computes joint angles
24
  - **Joint Mode**: Direct control of individual joint positions
25
  - End-effector position and orientation tracking
 
26
 
27
  ## Features
28
 
@@ -159,7 +160,8 @@ docker run --gpus all -p 3004:3004 \
159
  - **E**: Strafe right
160
 
161
  ### Robot Arm (UR5)
162
- - **IK Mode**: Use XYZ sliders to set end-effector target position
 
163
  - **Joint Mode**: Use J1-J6 sliders to control individual joints
164
  - **Gripper**: Open/Close buttons for Robotiq gripper
165
  - **Home**: Return to home position (joint mode)
@@ -399,12 +401,25 @@ All messages are JSON with `{type, data}` structure:
399
 
400
  #### UR5-Specific Messages
401
 
402
- **`arm_target`** (IK mode - set end-effector target):
403
  ```json
404
  {"type": "arm_target", "data": {"x": 0.4, "y": 0.0, "z": 0.6}}
405
  ```
406
  - `x`, `y`, `z`: Target position in meters
407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  **`joint_positions`** (Joint mode - direct joint control):
409
  ```json
410
  {"type": "joint_positions", "data": {"positions": [-1.57, -1.57, 1.57, -1.57, -1.57, 0.0]}}
@@ -450,14 +465,20 @@ For robot arm (UR5):
450
  "data": {
451
  "robot": "ur5",
452
  "end_effector": {"x": 0.4, "y": 0.0, "z": 0.6},
 
453
  "target": {"x": 0.4, "y": 0.0, "z": 0.6},
 
454
  "gripper": 128,
455
  "joint_positions": [-1.57, -1.57, 1.57, -1.57, -1.57, 0.0],
456
  "control_mode": "ik",
 
457
  "steps": 1234
458
  }
459
  }
460
  ```
 
 
 
461
 
462
  ### HTTP Endpoints
463
 
 
20
  - 6 degrees of freedom (6-axis industrial arm)
21
  - Robotiq 2F-85 gripper
22
  - Two control modes:
23
+ - **IK Mode**: Set target XYZ position and orientation (Roll/Pitch/Yaw), inverse kinematics computes joint angles using damped least squares with 6-DOF Jacobian
24
  - **Joint Mode**: Direct control of individual joint positions
25
  - End-effector position and orientation tracking
26
+ - Full 6-DOF IK with orientation control (can be toggled on/off)
27
 
28
  ## Features
29
 
 
160
  - **E**: Strafe right
161
 
162
  ### Robot Arm (UR5)
163
+ - **IK Mode**: Use XYZ sliders to set end-effector target position, RPY sliders for orientation
164
+ - **Orientation Control**: Toggle checkbox to enable/disable 6-DOF orientation tracking
165
  - **Joint Mode**: Use J1-J6 sliders to control individual joints
166
  - **Gripper**: Open/Close buttons for Robotiq gripper
167
  - **Home**: Return to home position (joint mode)
 
401
 
402
  #### UR5-Specific Messages
403
 
404
+ **`arm_target`** (IK mode - set end-effector target position):
405
  ```json
406
  {"type": "arm_target", "data": {"x": 0.4, "y": 0.0, "z": 0.6}}
407
  ```
408
  - `x`, `y`, `z`: Target position in meters
409
 
410
+ **`arm_orientation`** (IK mode - set end-effector target orientation):
411
+ ```json
412
+ {"type": "arm_orientation", "data": {"roll": 0.0, "pitch": 1.57, "yaw": 0.0}}
413
+ ```
414
+ - `roll`, `pitch`, `yaw`: Target orientation in radians (XYZ Euler convention)
415
+ - Default pointing down: `pitch = 1.57` (π/2)
416
+
417
+ **`use_orientation`** (Toggle orientation control):
418
+ ```json
419
+ {"type": "use_orientation", "data": {"enabled": true}}
420
+ ```
421
+ - `enabled`: `true` for 6-DOF IK (position + orientation), `false` for 3-DOF IK (position only)
422
+
423
  **`joint_positions`** (Joint mode - direct joint control):
424
  ```json
425
  {"type": "joint_positions", "data": {"positions": [-1.57, -1.57, 1.57, -1.57, -1.57, 0.0]}}
 
465
  "data": {
466
  "robot": "ur5",
467
  "end_effector": {"x": 0.4, "y": 0.0, "z": 0.6},
468
+ "ee_orientation": {"w": 0.5, "x": 0.5, "y": 0.5, "z": 0.5},
469
  "target": {"x": 0.4, "y": 0.0, "z": 0.6},
470
+ "target_orientation": {"roll": 0.0, "pitch": 1.57, "yaw": 0.0},
471
  "gripper": 128,
472
  "joint_positions": [-1.57, -1.57, 1.57, -1.57, -1.57, 0.0],
473
  "control_mode": "ik",
474
+ "use_orientation": true,
475
  "steps": 1234
476
  }
477
  }
478
  ```
479
+ - `ee_orientation`: Current end-effector orientation as quaternion [w, x, y, z]
480
+ - `target_orientation`: Target orientation as Euler angles (radians)
481
+ - `use_orientation`: Whether 6-DOF orientation control is enabled
482
 
483
  ### HTTP Endpoints
484
 
mujoco_server.py CHANGED
@@ -168,20 +168,26 @@ def broadcast_state():
168
  # UR5 has different state structure
169
  if current_robot == "ur5":
170
  ee_pos = env.get_end_effector_pos()
 
171
  target = env.get_target()
 
172
  gripper = env.get_gripper()
173
  joint_pos = env.get_joint_positions()
174
  control_mode = env.get_control_mode()
 
175
 
176
  state_msg = json.dumps({
177
  'type': 'state',
178
  'data': {
179
  'robot': current_robot,
180
  'end_effector': {'x': float(ee_pos[0]), 'y': float(ee_pos[1]), 'z': float(ee_pos[2])},
 
181
  'target': {'x': float(target[0]), 'y': float(target[1]), 'z': float(target[2])},
 
182
  'gripper': float(gripper),
183
  'joint_positions': [float(j) for j in joint_pos],
184
  'control_mode': control_mode,
 
185
  'steps': int(steps)
186
  }
187
  })
@@ -379,10 +385,12 @@ def handle_ws_message(data):
379
  elif msg_type == 'gripper':
380
  payload = data.get('data', {})
381
  action = payload.get('action', 'open')
 
 
382
  if action == 'open':
383
- value = 0 # 0 = open
384
  elif action == 'close':
385
- value = 255 # 255 = closed
386
  else:
387
  value = payload.get('value', 128)
388
  with mujoco_lock:
@@ -404,6 +412,22 @@ def handle_ws_message(data):
404
  if env is not None and current_robot == "ur5":
405
  env.set_joint_positions(positions)
406
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
 
408
  @sock.route(f'{API_PREFIX}/ws')
409
  def websocket_handler(ws):
@@ -465,12 +489,27 @@ def index():
465
  <!DOCTYPE html>
466
  <html>
467
  <head>
468
- <title>Nova Sim - Robot Simulator</title>
469
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  body, html {
471
  margin: 0; padding: 0; width: 100%; height: 100%;
472
- overflow: hidden; background: black;
473
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
474
  user-select: none;
475
  }
476
  .video-container {
@@ -481,71 +520,118 @@ def index():
481
  .video-container img { width: 100%; height: 100%; object-fit: cover; }
482
  .overlay {
483
  position: absolute; top: 20px; left: 20px;
484
- background: rgba(255, 255, 255, 0.1);
485
  backdrop-filter: blur(15px);
486
- padding: 25px; border-radius: 15px;
487
- box-shadow: 0 8px 32px rgba(0,0,0,0.5);
488
- color: white; border: 1px solid rgba(255, 255, 255, 0.1);
489
  z-index: 100; width: 320px;
490
  }
491
- .overlay h2 { margin: 0 0 15px 0; font-size: 1.1em; font-weight: 300; letter-spacing: 1px; }
492
  .control-group { margin-bottom: 15px; }
493
- .control-group label { display: block; margin-bottom: 5px; font-size: 0.8em; opacity: 0.7; }
494
  .slider-row { display: flex; align-items: center; gap: 12px; }
495
- input[type=range] { flex-grow: 1; cursor: pointer; opacity: 0.8; }
496
- .val-display { width: 60px; font-family: monospace; font-size: 0.9em; text-align: right; }
 
 
 
497
  button {
498
  flex: 1; padding: 10px;
499
- background: rgba(255, 255, 255, 0.1);
500
- color: white; border: 1px solid rgba(255, 255, 255, 0.2);
501
  border-radius: 6px; cursor: pointer; transition: all 0.2s;
502
  font-size: 0.85em;
503
  }
504
- button:hover { background: rgba(255, 255, 255, 0.2); }
505
- button.danger { background: rgba(231, 76, 60, 0.5); border-color: rgba(231, 76, 60, 0.3); }
506
- button.danger:hover { background: rgba(231, 76, 60, 0.7); }
507
- .stats { margin-top: 15px; font-size: 0.8em; opacity: 0.8; line-height: 1.6; }
508
- .hint { position: absolute; bottom: 20px; right: 20px; color: rgba(255,255,255,0.5); font-size: 0.8em; pointer-events: none; text-align: right; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  .rl-buttons { display: flex; flex-direction: column; gap: 5px; margin: 10px 0; }
510
  .rl-row { display: flex; justify-content: center; gap: 5px; }
511
  .rl-btn {
512
  padding: 12px 16px; min-width: 80px;
513
- background: rgba(46, 204, 113, 0.4); border: 1px solid rgba(46, 204, 113, 0.5);
514
- color: white; border-radius: 6px; cursor: pointer; transition: all 0.15s;
515
  font-size: 0.85em; font-weight: 500;
516
  }
517
- .rl-btn:hover { background: rgba(46, 204, 113, 0.6); }
518
- .rl-btn:active { background: rgba(46, 204, 113, 0.8); transform: scale(0.95); }
519
- .rl-btn.stop { background: rgba(231, 76, 60, 0.4); border-color: rgba(231, 76, 60, 0.5); }
520
- .rl-btn.stop:hover { background: rgba(231, 76, 60, 0.6); }
521
- .cmd-display { font-family: monospace; font-size: 0.8em; opacity: 0.8; margin-top: 8px; }
522
  .connection-status {
523
  position: absolute; top: 20px; right: 20px;
524
  padding: 8px 15px; border-radius: 20px;
525
  font-size: 0.75em; font-weight: 500;
526
- background: rgba(46, 204, 113, 0.4);
527
- color: white; border: 1px solid rgba(46, 204, 113, 0.5);
528
  }
529
  .connection-status.disconnected {
530
- background: rgba(231, 76, 60, 0.4);
531
- border-color: rgba(231, 76, 60, 0.5);
532
  }
533
  .camera-controls { margin-top: 15px; }
534
  select {
535
  width: 100%; padding: 10px;
536
- background: rgba(255, 255, 255, 0.1);
537
- color: white; border: 1px solid rgba(255, 255, 255, 0.2);
538
  border-radius: 6px; cursor: pointer;
539
  font-size: 0.9em;
540
  }
541
- select option { background: #333; color: white; }
542
  .robot-selector { margin-bottom: 20px; }
543
  .robot-info {
544
  padding: 10px;
545
- background: rgba(52, 152, 219, 0.3);
546
  border-radius: 6px;
547
  margin-top: 10px;
548
  font-size: 0.8em;
 
549
  }
550
  .arm-controls { display: none; }
551
  .arm-controls.active { display: block; }
@@ -559,12 +645,12 @@ def index():
559
  .mode-toggle { display: flex; gap: 5px; margin-bottom: 15px; }
560
  .mode-toggle button {
561
  flex: 1; padding: 8px;
562
- background: rgba(255, 255, 255, 0.1);
563
- border: 1px solid rgba(255, 255, 255, 0.2);
564
  }
565
  .mode-toggle button.active {
566
- background: rgba(52, 152, 219, 0.5);
567
- border-color: rgba(52, 152, 219, 0.7);
568
  }
569
  .ik-controls, .joint-controls { display: none; }
570
  .ik-controls.active, .joint-controls.active { display: block; }
@@ -579,52 +665,81 @@ def index():
579
 
580
  <div class="connection-status" id="conn_status">Connecting...</div>
581
 
582
- <div class="overlay">
583
- <h2 id="robot_title">Unitree G1 Humanoid</h2>
584
-
585
- <div class="robot-selector">
586
- <label>Select Robot</label>
587
- <select id="robot_select" onchange="switchRobot()">
588
- <option value="g1">Unitree G1 (Humanoid)</option>
589
- <option value="spot">Boston Dynamics Spot (Quadruped)</option>
590
- <option value="ur5">Universal Robots UR5e (Arm)</option>
591
- </select>
592
- <div class="robot-info" id="robot_info">
593
- 29 DOF humanoid with RL walking policy
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  </div>
595
  </div>
 
 
 
 
 
 
 
596
 
597
- <!-- Locomotion controls (G1, Spot) -->
598
- <div class="locomotion-controls" id="locomotion_controls">
599
- <div class="control-group">
600
- <label>Walking Controls (WASD or buttons)</label>
601
- <div class="rl-buttons">
602
- <div class="rl-row">
603
- <button class="rl-btn" onmousedown="setCmd(0.8, 0, 0)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(0.8, 0, 0)" ontouchend="setCmd(0,0,0)">W Forward</button>
604
- </div>
605
- <div class="rl-row">
606
- <button class="rl-btn" onmousedown="setCmd(0, 0, 1.2)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(0, 0, 1.2)" ontouchend="setCmd(0,0,0)">A Turn L</button>
607
- <button class="rl-btn" onmousedown="setCmd(-0.5, 0, 0)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(-0.5, 0, 0)" ontouchend="setCmd(0,0,0)">S Back</button>
608
- <button class="rl-btn" onmousedown="setCmd(0, 0, -1.2)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(0, 0, -1.2)" ontouchend="setCmd(0,0,0)">D Turn R</button>
609
- </div>
610
- <div class="rl-row">
611
- <button class="rl-btn" onmousedown="setCmd(0, 0.4, 0)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(0, 0.4, 0)" ontouchend="setCmd(0,0,0)">Q Strafe L</button>
612
- <button class="rl-btn stop" onclick="setCmd(0, 0, 0)">Stop</button>
613
- <button class="rl-btn" onmousedown="setCmd(0, -0.4, 0)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(0, -0.4, 0)" ontouchend="setCmd(0,0,0)">E Strafe R</button>
614
- </div>
615
- </div>
616
- <div class="cmd-display">
617
- Cmd: vx=<span id="cmd_vx">0.0</span> vy=<span id="cmd_vy">0.0</span> yaw=<span id="cmd_yaw">0.0</span>
618
  </div>
619
  </div>
620
 
621
- <div class="stats" id="locomotion_stats">
622
- <strong>Robot State:</strong><br>
623
- Base Height: <span id="height_val">0.00</span> m<br>
624
- Upright Score: <span id="upright_val">0.00</span><br>
625
- Step Count: <span id="step_val">0</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  </div>
627
- </div>
628
 
629
  <!-- Arm controls (UR5) -->
630
  <div class="arm-controls" id="arm_controls">
@@ -653,8 +768,32 @@ def index():
653
  </div>
654
  <div class="slider-row">
655
  <label>Z</label>
656
- <input type="range" id="target_z" min="0.3" max="1.0" step="0.01" value="0.6" oninput="updateTarget()">
657
- <span class="val-display" id="target_z_val">0.60</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  </div>
659
  </div>
660
  </div>
@@ -667,18 +806,18 @@ def index():
667
  <div class="joint-sliders">
668
  <div class="slider-row">
669
  <label>J1</label>
670
- <input type="range" id="joint_0" min="-6.28" max="6.28" step="0.01" value="-1.57" oninput="updateJoints()">
671
- <span class="val-display" id="joint_0_val">-1.57</span>
672
  </div>
673
  <div class="slider-row">
674
  <label>J2</label>
675
- <input type="range" id="joint_1" min="-6.28" max="6.28" step="0.01" value="-1.57" oninput="updateJoints()">
676
- <span class="val-display" id="joint_1_val">-1.57</span>
677
  </div>
678
  <div class="slider-row">
679
  <label>J3</label>
680
- <input type="range" id="joint_2" min="-3.14" max="3.14" step="0.01" value="1.57" oninput="updateJoints()">
681
- <span class="val-display" id="joint_2_val">1.57</span>
682
  </div>
683
  <div class="slider-row">
684
  <label>J4</label>
@@ -707,32 +846,9 @@ def index():
707
  <button class="rl-btn" onclick="setGripper('close')">Close</button>
708
  </div>
709
  </div>
710
-
711
- <div class="stats" id="arm_stats">
712
- <strong>Arm State:</strong><br>
713
- End Effector: <span id="ee_pos">0.00, 0.00, 0.00</span><br>
714
- Joints: <span id="joint_pos_display">0.00, 0.00, 0.00, 0.00, 0.00, 0.00</span><br>
715
- Mode: <span id="control_mode_display">IK</span> | Gripper: <span id="gripper_val">50%</span><br>
716
- Step Count: <span id="arm_step_val">0</span>
717
- </div>
718
  </div>
719
 
720
- <button class="danger" onclick="resetEnv()">Reset Environment</button>
721
-
722
- <div class="camera-controls">
723
- <div class="control-group">
724
- <label>Camera Distance</label>
725
- <div class="slider-row">
726
- <input type="range" id="cam_dist" min="1" max="10" step="0.1" value="3.0">
727
- <span class="val-display" id="cam_dist_val">3.0</span>
728
- </div>
729
- </div>
730
- <div class="control-group" style="margin-top: 10px;">
731
- <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
732
- <input type="checkbox" id="cam_follow" checked onchange="setCameraFollow()">
733
- Camera Follow Robot
734
- </label>
735
- </div>
736
  </div>
737
  </div>
738
 
@@ -808,8 +924,18 @@ def index():
808
  const ee = data.end_effector;
809
  document.getElementById('ee_pos').innerText =
810
  ee.x.toFixed(2) + ', ' + ee.y.toFixed(2) + ', ' + ee.z.toFixed(2);
 
 
 
 
 
 
 
 
 
 
811
  document.getElementById('gripper_val').innerText =
812
- ((255 - data.gripper) / 255 * 100).toFixed(0) + '%';
813
  document.getElementById('arm_step_val').innerText = data.steps;
814
 
815
  // Update joint position display
@@ -828,6 +954,15 @@ def index():
828
  setControlMode(data.control_mode);
829
  }
830
  }
 
 
 
 
 
 
 
 
 
831
  } else {
832
  // Locomotion state
833
  heightVal.innerText = data.base_height.toFixed(2);
@@ -855,6 +990,30 @@ def index():
855
  }
856
  }
857
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
858
  function updateRobotUI(robot) {
859
  robotTitle.innerText = robotTitles[robot] || robot;
860
  robotInfo.innerText = robotInfoText[robot] || '';
@@ -863,9 +1022,30 @@ def index():
863
  if (robot === 'ur5') {
864
  locomotionControls.classList.add('hidden');
865
  armControls.classList.add('active');
 
 
866
  } else {
867
  locomotionControls.classList.remove('hidden');
868
  armControls.classList.remove('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
869
  }
870
  }
871
 
@@ -924,6 +1104,22 @@ def index():
924
  send('arm_target', {x, y, z});
925
  }
926
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
927
  function updateJoints() {
928
  const positions = [];
929
  for (let i = 0; i < 6; i++) {
@@ -935,8 +1131,8 @@ def index():
935
  }
936
 
937
  function goToHome() {
938
- // UR5 home position
939
- const home = [-1.57, -1.57, 1.57, -1.57, -1.57, 0.0];
940
  for (let i = 0; i < 6; i++) {
941
  document.getElementById('joint_' + i).value = home[i];
942
  document.getElementById('joint_' + i + '_val').innerText = home[i].toFixed(2);
 
168
  # UR5 has different state structure
169
  if current_robot == "ur5":
170
  ee_pos = env.get_end_effector_pos()
171
+ ee_quat = env.get_end_effector_quat()
172
  target = env.get_target()
173
+ target_euler = env.get_target_orientation()
174
  gripper = env.get_gripper()
175
  joint_pos = env.get_joint_positions()
176
  control_mode = env.get_control_mode()
177
+ use_orientation = env.get_use_orientation()
178
 
179
  state_msg = json.dumps({
180
  'type': 'state',
181
  'data': {
182
  'robot': current_robot,
183
  'end_effector': {'x': float(ee_pos[0]), 'y': float(ee_pos[1]), 'z': float(ee_pos[2])},
184
+ 'ee_orientation': {'w': float(ee_quat[0]), 'x': float(ee_quat[1]), 'y': float(ee_quat[2]), 'z': float(ee_quat[3])},
185
  'target': {'x': float(target[0]), 'y': float(target[1]), 'z': float(target[2])},
186
+ 'target_orientation': {'roll': float(target_euler[0]), 'pitch': float(target_euler[1]), 'yaw': float(target_euler[2])},
187
  'gripper': float(gripper),
188
  'joint_positions': [float(j) for j in joint_pos],
189
  'control_mode': control_mode,
190
+ 'use_orientation': use_orientation,
191
  'steps': int(steps)
192
  }
193
  })
 
385
  elif msg_type == 'gripper':
386
  payload = data.get('data', {})
387
  action = payload.get('action', 'open')
388
+ # Robotiq 2F-85: higher control value = more open (joint angle increases)
389
+ # Control range 0-255 maps to joint angle 0-0.8 rad
390
  if action == 'open':
391
+ value = 255 # 255 = fully open
392
  elif action == 'close':
393
+ value = 0 # 0 = fully closed
394
  else:
395
  value = payload.get('value', 128)
396
  with mujoco_lock:
 
412
  if env is not None and current_robot == "ur5":
413
  env.set_joint_positions(positions)
414
 
415
+ elif msg_type == 'arm_orientation':
416
+ payload = data.get('data', {})
417
+ roll = payload.get('roll', 0.0)
418
+ pitch = payload.get('pitch', np.pi/2)
419
+ yaw = payload.get('yaw', 0.0)
420
+ with mujoco_lock:
421
+ if env is not None and current_robot == "ur5":
422
+ env.set_target_orientation(roll, pitch, yaw)
423
+
424
+ elif msg_type == 'use_orientation':
425
+ payload = data.get('data', {})
426
+ use = payload.get('enabled', True)
427
+ with mujoco_lock:
428
+ if env is not None and current_robot == "ur5":
429
+ env.set_use_orientation(use)
430
+
431
 
432
  @sock.route(f'{API_PREFIX}/ws')
433
  def websocket_handler(ws):
 
489
  <!DOCTYPE html>
490
  <html>
491
  <head>
492
+ <title>Nova Sim - Wandelbots Robot Simulator</title>
493
  <style>
494
+ /* Wandelbots Corporate Design Colors:
495
+ Primary Dark: #01040f (Blue Charcoal)
496
+ Light/Secondary: #bcbeec (Spindle - lavender)
497
+ Accent: #211c44 (Port Gore - deep purple)
498
+ Logo Dark: #181838
499
+ */
500
+ :root {
501
+ --wb-primary: #01040f;
502
+ --wb-secondary: #bcbeec;
503
+ --wb-accent: #211c44;
504
+ --wb-logo: #181838;
505
+ --wb-highlight: #8b7fef;
506
+ --wb-success: #7c6bef;
507
+ --wb-danger: #ef6b6b;
508
+ }
509
  body, html {
510
  margin: 0; padding: 0; width: 100%; height: 100%;
511
+ overflow: hidden; background: var(--wb-primary);
512
+ font-family: Arial, Helvetica, sans-serif;
513
  user-select: none;
514
  }
515
  .video-container {
 
520
  .video-container img { width: 100%; height: 100%; object-fit: cover; }
521
  .overlay {
522
  position: absolute; top: 20px; left: 20px;
523
+ background: rgba(33, 28, 68, 0.85);
524
  backdrop-filter: blur(15px);
525
+ padding: 25px; border-radius: 12px;
526
+ box-shadow: 0 8px 32px rgba(1, 4, 15, 0.6);
527
+ color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.15);
528
  z-index: 100; width: 320px;
529
  }
530
+ .overlay h2 { margin: 0 0 15px 0; font-size: 1.1em; font-weight: 600; letter-spacing: 0.5px; color: #fff; }
531
  .control-group { margin-bottom: 15px; }
532
+ .control-group label { display: block; margin-bottom: 5px; font-size: 0.8em; opacity: 0.8; color: var(--wb-secondary); }
533
  .slider-row { display: flex; align-items: center; gap: 12px; }
534
+ input[type=range] {
535
+ flex-grow: 1; cursor: pointer;
536
+ accent-color: var(--wb-highlight);
537
+ }
538
+ .val-display { width: 60px; font-family: monospace; font-size: 0.9em; text-align: right; color: #fff; }
539
  button {
540
  flex: 1; padding: 10px;
541
+ background: rgba(188, 190, 236, 0.1);
542
+ color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.25);
543
  border-radius: 6px; cursor: pointer; transition: all 0.2s;
544
  font-size: 0.85em;
545
  }
546
+ button:hover { background: rgba(188, 190, 236, 0.2); border-color: rgba(188, 190, 236, 0.4); }
547
+ button.danger { background: rgba(239, 107, 107, 0.4); border-color: rgba(239, 107, 107, 0.5); }
548
+ button.danger:hover { background: rgba(239, 107, 107, 0.6); }
549
+ .stats { margin-top: 15px; font-size: 0.8em; opacity: 0.9; line-height: 1.6; color: var(--wb-secondary); }
550
+ .hint { position: absolute; bottom: 20px; right: 20px; color: rgba(188, 190, 236, 0.5); font-size: 0.8em; pointer-events: none; text-align: right; }
551
+ /* State info panel - top right */
552
+ .state-panel {
553
+ position: absolute; top: 60px; right: 20px;
554
+ background: rgba(33, 28, 68, 0.85);
555
+ backdrop-filter: blur(15px);
556
+ padding: 15px 20px; border-radius: 10px;
557
+ box-shadow: 0 4px 20px rgba(1, 4, 15, 0.5);
558
+ color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.15);
559
+ z-index: 100; min-width: 200px;
560
+ font-size: 0.8em; line-height: 1.5;
561
+ }
562
+ .state-panel strong { color: #fff; }
563
+ /* Camera controls - bottom left */
564
+ .camera-panel {
565
+ position: absolute; bottom: 20px; left: 20px;
566
+ background: rgba(33, 28, 68, 0.85);
567
+ backdrop-filter: blur(15px);
568
+ padding: 15px 20px; border-radius: 10px;
569
+ box-shadow: 0 4px 20px rgba(1, 4, 15, 0.5);
570
+ color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.15);
571
+ z-index: 100; width: 280px;
572
+ }
573
+ .camera-panel .control-group { margin-bottom: 10px; }
574
+ .camera-panel .control-group:last-child { margin-bottom: 0; }
575
+ /* Collapsible panel header */
576
+ .panel-header {
577
+ display: flex; justify-content: space-between; align-items: center;
578
+ cursor: pointer; margin-bottom: 15px;
579
+ }
580
+ .panel-header h2 { margin: 0; }
581
+ .collapse-btn {
582
+ background: rgba(188, 190, 236, 0.2);
583
+ border: 1px solid rgba(188, 190, 236, 0.3);
584
+ color: var(--wb-secondary);
585
+ width: 28px; height: 28px;
586
+ border-radius: 6px; cursor: pointer;
587
+ display: flex; align-items: center; justify-content: center;
588
+ font-size: 1.1em; transition: all 0.2s;
589
+ }
590
+ .collapse-btn:hover { background: rgba(188, 190, 236, 0.3); }
591
+ .panel-content { transition: all 0.3s ease; overflow: hidden; }
592
+ .panel-content.collapsed { max-height: 0; opacity: 0; margin: 0; padding: 0; }
593
+ .overlay.collapsed { width: auto; min-width: 200px; }
594
  .rl-buttons { display: flex; flex-direction: column; gap: 5px; margin: 10px 0; }
595
  .rl-row { display: flex; justify-content: center; gap: 5px; }
596
  .rl-btn {
597
  padding: 12px 16px; min-width: 80px;
598
+ background: rgba(124, 107, 239, 0.4); border: 1px solid rgba(124, 107, 239, 0.5);
599
+ color: #fff; border-radius: 6px; cursor: pointer; transition: all 0.15s;
600
  font-size: 0.85em; font-weight: 500;
601
  }
602
+ .rl-btn:hover { background: rgba(124, 107, 239, 0.6); }
603
+ .rl-btn:active { background: rgba(124, 107, 239, 0.8); transform: scale(0.95); }
604
+ .rl-btn.stop { background: rgba(239, 107, 107, 0.4); border-color: rgba(239, 107, 107, 0.5); }
605
+ .rl-btn.stop:hover { background: rgba(239, 107, 107, 0.6); }
606
+ .cmd-display { font-family: monospace; font-size: 0.8em; opacity: 0.8; margin-top: 8px; color: var(--wb-secondary); }
607
  .connection-status {
608
  position: absolute; top: 20px; right: 20px;
609
  padding: 8px 15px; border-radius: 20px;
610
  font-size: 0.75em; font-weight: 500;
611
+ background: rgba(124, 107, 239, 0.5);
612
+ color: #fff; border: 1px solid rgba(124, 107, 239, 0.6);
613
  }
614
  .connection-status.disconnected {
615
+ background: rgba(239, 107, 107, 0.5);
616
+ border-color: rgba(239, 107, 107, 0.6);
617
  }
618
  .camera-controls { margin-top: 15px; }
619
  select {
620
  width: 100%; padding: 10px;
621
+ background: rgba(188, 190, 236, 0.1);
622
+ color: #fff; border: 1px solid rgba(188, 190, 236, 0.25);
623
  border-radius: 6px; cursor: pointer;
624
  font-size: 0.9em;
625
  }
626
+ select option { background: var(--wb-accent); color: #fff; }
627
  .robot-selector { margin-bottom: 20px; }
628
  .robot-info {
629
  padding: 10px;
630
+ background: rgba(139, 127, 239, 0.2);
631
  border-radius: 6px;
632
  margin-top: 10px;
633
  font-size: 0.8em;
634
+ border: 1px solid rgba(139, 127, 239, 0.3);
635
  }
636
  .arm-controls { display: none; }
637
  .arm-controls.active { display: block; }
 
645
  .mode-toggle { display: flex; gap: 5px; margin-bottom: 15px; }
646
  .mode-toggle button {
647
  flex: 1; padding: 8px;
648
+ background: rgba(188, 190, 236, 0.1);
649
+ border: 1px solid rgba(188, 190, 236, 0.2);
650
  }
651
  .mode-toggle button.active {
652
+ background: rgba(139, 127, 239, 0.5);
653
+ border-color: rgba(139, 127, 239, 0.7);
654
  }
655
  .ik-controls, .joint-controls { display: none; }
656
  .ik-controls.active, .joint-controls.active { display: block; }
 
665
 
666
  <div class="connection-status" id="conn_status">Connecting...</div>
667
 
668
+ <!-- State info panel - top right -->
669
+ <div class="state-panel" id="state_panel">
670
+ <div id="locomotion_state">
671
+ <strong>Robot State</strong><br>
672
+ Base Height: <span id="height_val">0.00</span> m<br>
673
+ Upright: <span id="upright_val">0.00</span><br>
674
+ Steps: <span id="step_val">0</span><br>
675
+ Cmd: <span id="cmd_vx">0.0</span>, <span id="cmd_vy">0.0</span>, <span id="cmd_yaw">0.0</span>
676
+ </div>
677
+ <div id="arm_state" style="display: none;">
678
+ <strong>Arm State</strong><br>
679
+ EE Pos: <span id="ee_pos">0.00, 0.00, 0.00</span><br>
680
+ EE Ori: <span id="ee_ori">0.00, 0.00, 0.00</span><br>
681
+ Gripper: <span id="gripper_val">50%</span><br>
682
+ Mode: <span id="control_mode_display">IK</span> | Steps: <span id="arm_step_val">0</span>
683
+ </div>
684
+ </div>
685
+
686
+ <!-- Camera controls - bottom left -->
687
+ <div class="camera-panel">
688
+ <div class="control-group">
689
+ <label>Camera Distance</label>
690
+ <div class="slider-row">
691
+ <input type="range" id="cam_dist" min="1" max="10" step="0.1" value="3.0">
692
+ <span class="val-display" id="cam_dist_val">3.0</span>
693
  </div>
694
  </div>
695
+ <div class="control-group">
696
+ <label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
697
+ <input type="checkbox" id="cam_follow" checked onchange="setCameraFollow()">
698
+ Camera Follow Robot
699
+ </label>
700
+ </div>
701
+ </div>
702
 
703
+ <div class="overlay" id="control_panel">
704
+ <div class="panel-header" onclick="togglePanel()">
705
+ <h2 id="robot_title">Unitree G1 Humanoid</h2>
706
+ <button class="collapse-btn" id="collapse_btn">-</button>
707
+ </div>
708
+
709
+ <div class="panel-content" id="panel_content">
710
+ <div class="robot-selector">
711
+ <label>Select Robot</label>
712
+ <select id="robot_select" onchange="switchRobot()">
713
+ <option value="g1">Unitree G1 (Humanoid)</option>
714
+ <option value="spot">Boston Dynamics Spot (Quadruped)</option>
715
+ <option value="ur5">Universal Robots UR5e (Arm)</option>
716
+ </select>
717
+ <div class="robot-info" id="robot_info">
718
+ 29 DOF humanoid with RL walking policy
 
 
 
 
 
719
  </div>
720
  </div>
721
 
722
+ <!-- Locomotion controls (G1, Spot) -->
723
+ <div class="locomotion-controls" id="locomotion_controls">
724
+ <div class="control-group">
725
+ <label>Walking Controls (WASD or buttons)</label>
726
+ <div class="rl-buttons">
727
+ <div class="rl-row">
728
+ <button class="rl-btn" onmousedown="setCmd(0.8, 0, 0)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(0.8, 0, 0)" ontouchend="setCmd(0,0,0)">W Forward</button>
729
+ </div>
730
+ <div class="rl-row">
731
+ <button class="rl-btn" onmousedown="setCmd(0, 0, 1.2)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(0, 0, 1.2)" ontouchend="setCmd(0,0,0)">A Turn L</button>
732
+ <button class="rl-btn" onmousedown="setCmd(-0.5, 0, 0)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(-0.5, 0, 0)" ontouchend="setCmd(0,0,0)">S Back</button>
733
+ <button class="rl-btn" onmousedown="setCmd(0, 0, -1.2)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(0, 0, -1.2)" ontouchend="setCmd(0,0,0)">D Turn R</button>
734
+ </div>
735
+ <div class="rl-row">
736
+ <button class="rl-btn" onmousedown="setCmd(0, 0.4, 0)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(0, 0.4, 0)" ontouchend="setCmd(0,0,0)">Q Strafe L</button>
737
+ <button class="rl-btn stop" onclick="setCmd(0, 0, 0)">Stop</button>
738
+ <button class="rl-btn" onmousedown="setCmd(0, -0.4, 0)" onmouseup="setCmd(0,0,0)" ontouchstart="setCmd(0, -0.4, 0)" ontouchend="setCmd(0,0,0)">E Strafe R</button>
739
+ </div>
740
+ </div>
741
+ </div>
742
  </div>
 
743
 
744
  <!-- Arm controls (UR5) -->
745
  <div class="arm-controls" id="arm_controls">
 
768
  </div>
769
  <div class="slider-row">
770
  <label>Z</label>
771
+ <input type="range" id="target_z" min="0.1" max="1.0" step="0.01" value="0.4" oninput="updateTarget()">
772
+ <span class="val-display" id="target_z_val">0.40</span>
773
+ </div>
774
+ </div>
775
+ </div>
776
+ <div class="control-group">
777
+ <label style="display: flex; align-items: center; gap: 8px;">
778
+ Target Orientation (RPY)
779
+ <input type="checkbox" id="use_orientation" checked onchange="setUseOrientation()">
780
+ <span style="font-size: 0.75em; opacity: 0.7;">Enable</span>
781
+ </label>
782
+ <div class="target-sliders" id="orientation_sliders">
783
+ <div class="slider-row">
784
+ <label>R</label>
785
+ <input type="range" id="target_roll" min="-3.14" max="3.14" step="0.05" value="0.0" oninput="updateOrientation()">
786
+ <span class="val-display" id="target_roll_val">0.00</span>
787
+ </div>
788
+ <div class="slider-row">
789
+ <label>P</label>
790
+ <input type="range" id="target_pitch" min="-3.14" max="3.14" step="0.05" value="1.57" oninput="updateOrientation()">
791
+ <span class="val-display" id="target_pitch_val">1.57</span>
792
+ </div>
793
+ <div class="slider-row">
794
+ <label>Y</label>
795
+ <input type="range" id="target_yaw" min="-3.14" max="3.14" step="0.05" value="0.0" oninput="updateOrientation()">
796
+ <span class="val-display" id="target_yaw_val">0.00</span>
797
  </div>
798
  </div>
799
  </div>
 
806
  <div class="joint-sliders">
807
  <div class="slider-row">
808
  <label>J1</label>
809
+ <input type="range" id="joint_0" min="-6.28" max="6.28" step="0.01" value="0.0" oninput="updateJoints()">
810
+ <span class="val-display" id="joint_0_val">0.00</span>
811
  </div>
812
  <div class="slider-row">
813
  <label>J2</label>
814
+ <input type="range" id="joint_1" min="-6.28" max="6.28" step="0.01" value="-2.0" oninput="updateJoints()">
815
+ <span class="val-display" id="joint_1_val">-2.00</span>
816
  </div>
817
  <div class="slider-row">
818
  <label>J3</label>
819
+ <input type="range" id="joint_2" min="-3.14" max="3.14" step="0.01" value="2.0" oninput="updateJoints()">
820
+ <span class="val-display" id="joint_2_val">2.00</span>
821
  </div>
822
  <div class="slider-row">
823
  <label>J4</label>
 
846
  <button class="rl-btn" onclick="setGripper('close')">Close</button>
847
  </div>
848
  </div>
 
 
 
 
 
 
 
 
849
  </div>
850
 
851
+ <button class="danger" style="width: 100%; margin-top: 15px;" onclick="resetEnv()">Reset Environment</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
  </div>
853
  </div>
854
 
 
924
  const ee = data.end_effector;
925
  document.getElementById('ee_pos').innerText =
926
  ee.x.toFixed(2) + ', ' + ee.y.toFixed(2) + ', ' + ee.z.toFixed(2);
927
+
928
+ // EE Orientation - convert quaternion to euler for display
929
+ if (data.ee_orientation) {
930
+ const q = data.ee_orientation;
931
+ const euler = quatToEuler(q.w, q.x, q.y, q.z);
932
+ document.getElementById('ee_ori').innerText =
933
+ euler[0].toFixed(2) + ', ' + euler[1].toFixed(2) + ', ' + euler[2].toFixed(2);
934
+ }
935
+
936
+ // Gripper: 0=closed, 255=open
937
  document.getElementById('gripper_val').innerText =
938
+ (data.gripper / 255 * 100).toFixed(0) + '% open';
939
  document.getElementById('arm_step_val').innerText = data.steps;
940
 
941
  // Update joint position display
 
954
  setControlMode(data.control_mode);
955
  }
956
  }
957
+
958
+ // Sync use_orientation checkbox
959
+ if (data.use_orientation !== undefined) {
960
+ const checkbox = document.getElementById('use_orientation');
961
+ if (checkbox.checked !== data.use_orientation) {
962
+ checkbox.checked = data.use_orientation;
963
+ document.getElementById('orientation_sliders').style.opacity = data.use_orientation ? '1' : '0.4';
964
+ }
965
+ }
966
  } else {
967
  // Locomotion state
968
  heightVal.innerText = data.base_height.toFixed(2);
 
990
  }
991
  }
992
 
993
+ // Convert quaternion to euler angles (XYZ convention)
994
+ function quatToEuler(w, x, y, z) {
995
+ // Roll (x-axis rotation)
996
+ const sinr_cosp = 2 * (w * x + y * z);
997
+ const cosr_cosp = 1 - 2 * (x * x + y * y);
998
+ const roll = Math.atan2(sinr_cosp, cosr_cosp);
999
+
1000
+ // Pitch (y-axis rotation)
1001
+ const sinp = 2 * (w * y - z * x);
1002
+ let pitch;
1003
+ if (Math.abs(sinp) >= 1) {
1004
+ pitch = Math.sign(sinp) * Math.PI / 2;
1005
+ } else {
1006
+ pitch = Math.asin(sinp);
1007
+ }
1008
+
1009
+ // Yaw (z-axis rotation)
1010
+ const siny_cosp = 2 * (w * z + x * y);
1011
+ const cosy_cosp = 1 - 2 * (y * y + z * z);
1012
+ const yaw = Math.atan2(siny_cosp, cosy_cosp);
1013
+
1014
+ return [roll, pitch, yaw];
1015
+ }
1016
+
1017
  function updateRobotUI(robot) {
1018
  robotTitle.innerText = robotTitles[robot] || robot;
1019
  robotInfo.innerText = robotInfoText[robot] || '';
 
1022
  if (robot === 'ur5') {
1023
  locomotionControls.classList.add('hidden');
1024
  armControls.classList.add('active');
1025
+ document.getElementById('locomotion_state').style.display = 'none';
1026
+ document.getElementById('arm_state').style.display = 'block';
1027
  } else {
1028
  locomotionControls.classList.remove('hidden');
1029
  armControls.classList.remove('active');
1030
+ document.getElementById('locomotion_state').style.display = 'block';
1031
+ document.getElementById('arm_state').style.display = 'none';
1032
+ }
1033
+ }
1034
+
1035
+ let panelCollapsed = false;
1036
+ function togglePanel() {
1037
+ panelCollapsed = !panelCollapsed;
1038
+ const content = document.getElementById('panel_content');
1039
+ const btn = document.getElementById('collapse_btn');
1040
+ const panel = document.getElementById('control_panel');
1041
+ if (panelCollapsed) {
1042
+ content.classList.add('collapsed');
1043
+ panel.classList.add('collapsed');
1044
+ btn.innerText = '+';
1045
+ } else {
1046
+ content.classList.remove('collapsed');
1047
+ panel.classList.remove('collapsed');
1048
+ btn.innerText = '-';
1049
  }
1050
  }
1051
 
 
1104
  send('arm_target', {x, y, z});
1105
  }
1106
 
1107
+ function updateOrientation() {
1108
+ const roll = parseFloat(document.getElementById('target_roll').value);
1109
+ const pitch = parseFloat(document.getElementById('target_pitch').value);
1110
+ const yaw = parseFloat(document.getElementById('target_yaw').value);
1111
+ document.getElementById('target_roll_val').innerText = roll.toFixed(2);
1112
+ document.getElementById('target_pitch_val').innerText = pitch.toFixed(2);
1113
+ document.getElementById('target_yaw_val').innerText = yaw.toFixed(2);
1114
+ send('arm_orientation', {roll, pitch, yaw});
1115
+ }
1116
+
1117
+ function setUseOrientation() {
1118
+ const enabled = document.getElementById('use_orientation').checked;
1119
+ document.getElementById('orientation_sliders').style.opacity = enabled ? '1' : '0.4';
1120
+ send('use_orientation', {enabled});
1121
+ }
1122
+
1123
  function updateJoints() {
1124
  const positions = [];
1125
  for (let i = 0; i < 6; i++) {
 
1131
  }
1132
 
1133
  function goToHome() {
1134
+ // UR5 home position - reaching forward with gripper down
1135
+ const home = [0.0, -2.0, 2.0, -1.57, -1.57, 0.0];
1136
  for (let i = 0; i < 6; i++) {
1137
  document.getElementById('joint_' + i).value = home[i];
1138
  document.getElementById('joint_' + i + '_val').innerText = home[i].toFixed(2);
robots/g1/scene.xml CHANGED
@@ -4,17 +4,25 @@
4
 
5
  <statistic center="0 0 0.5" extent="2.0"/>
6
 
 
 
 
 
 
 
7
  <visual>
8
- <headlight diffuse="0.6 0.6 0.6" ambient="0.3 0.3 0.3" specular="0 0 0"/>
9
- <rgba haze="0.15 0.25 0.35 1"/>
10
  <global azimuth="-130" elevation="-20" offwidth="1280" offheight="720"/>
11
  </visual>
12
 
13
  <asset>
14
- <texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072"/>
15
- <texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3"
16
- markrgb="0.8 0.8 0.8" width="300" height="300"/>
17
- <material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2"/>
 
 
18
  </asset>
19
 
20
  <worldbody>
 
4
 
5
  <statistic center="0 0 0.5" extent="2.0"/>
6
 
7
+ <!-- Wandelbots Corporate Design Colors:
8
+ Primary Dark: #01040f (0.004, 0.016, 0.059)
9
+ Light/Secondary: #bcbeec (0.737, 0.745, 0.925)
10
+ Accent: #211c44 (0.129, 0.110, 0.267)
11
+ Highlight: #8b7fef (0.545, 0.498, 0.937)
12
+ -->
13
  <visual>
14
+ <headlight diffuse="0.6 0.6 0.6" ambient="0.35 0.35 0.4" specular="0 0 0"/>
15
+ <rgba haze="0.02 0.04 0.12 1"/>
16
  <global azimuth="-130" elevation="-20" offwidth="1280" offheight="720"/>
17
  </visual>
18
 
19
  <asset>
20
+ <!-- Wandelbots gradient skybox - deep purple to near black -->
21
+ <texture type="skybox" builtin="gradient" rgb1="0.13 0.11 0.27" rgb2="0.004 0.016 0.059" width="512" height="3072"/>
22
+ <!-- Ground with Wandelbots purple accent -->
23
+ <texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.08 0.07 0.15" rgb2="0.04 0.04 0.08"
24
+ markrgb="0.55 0.5 0.94" width="300" height="300"/>
25
+ <material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.15"/>
26
  </asset>
27
 
28
  <worldbody>
robots/spot/model/scene.xml CHANGED
@@ -3,18 +3,26 @@
3
 
4
  <statistic center="0.15 0.1 0.38" extent=".8" meansize="0.05"/>
5
 
 
 
 
 
 
 
6
  <visual>
7
- <headlight diffuse="0.6 0.6 0.6" ambient="0.3 0.3 0.3" specular="0 0 0"/>
8
- <rgba haze="0.15 0.25 0.35 1"/>
9
  <global azimuth="220" elevation="-10"/>
10
  <quality shadowsize="8192"/>
11
  </visual>
12
 
13
  <asset>
14
- <texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072"/>
15
- <texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3"
16
- markrgb="0.8 0.8 0.8" width="300" height="300"/>
17
- <material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2"/>
 
 
18
  </asset>
19
 
20
  <worldbody>
 
3
 
4
  <statistic center="0.15 0.1 0.38" extent=".8" meansize="0.05"/>
5
 
6
+ <!-- Wandelbots Corporate Design Colors:
7
+ Primary Dark: #01040f (0.004, 0.016, 0.059)
8
+ Light/Secondary: #bcbeec (0.737, 0.745, 0.925)
9
+ Accent: #211c44 (0.129, 0.110, 0.267)
10
+ Highlight: #8b7fef (0.545, 0.498, 0.937)
11
+ -->
12
  <visual>
13
+ <headlight diffuse="0.6 0.6 0.6" ambient="0.35 0.35 0.4" specular="0 0 0"/>
14
+ <rgba haze="0.02 0.04 0.12 1"/>
15
  <global azimuth="220" elevation="-10"/>
16
  <quality shadowsize="8192"/>
17
  </visual>
18
 
19
  <asset>
20
+ <!-- Wandelbots gradient skybox - deep purple to near black -->
21
+ <texture type="skybox" builtin="gradient" rgb1="0.13 0.11 0.27" rgb2="0.004 0.016 0.059" width="512" height="3072"/>
22
+ <!-- Ground with Wandelbots purple accent -->
23
+ <texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.08 0.07 0.15" rgb2="0.04 0.04 0.08"
24
+ markrgb="0.55 0.5 0.94" width="300" height="300"/>
25
+ <material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.15"/>
26
  </asset>
27
 
28
  <worldbody>
robots/ur5/controllers/ik_controller.py CHANGED
@@ -73,8 +73,27 @@ class IKController:
73
  """Get current end-effector position."""
74
  return self.data.site_xpos[self.ee_site_id].copy()
75
 
76
- def compute_jacobian(self):
77
- """Compute the Jacobian for the end-effector site."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  # Zero out arrays
79
  self.jacp.fill(0)
80
  self.jacr.fill(0)
@@ -82,12 +101,19 @@ class IKController:
82
  # Compute Jacobian using MuJoCo
83
  mujoco.mj_jacSite(self.model, self.data, self.jacp, self.jacr, self.ee_site_id)
84
 
85
- # Extract columns corresponding to controlled joints
86
- jac = np.zeros((3, self.num_joints))
87
- for i, dof_id in enumerate(self.dof_ids):
88
- jac[:, i] = self.jacp[:, dof_id]
89
-
90
- return jac
 
 
 
 
 
 
 
91
 
92
  def compute_ik(self, target_pos, current_qpos=None):
93
  """
@@ -140,7 +166,46 @@ class IKController:
140
 
141
  return qpos
142
 
143
- def compute_ik_with_orientation(self, target_pos, target_quat=None, current_qpos=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  """
145
  Compute joint positions to reach target pose (position + orientation).
146
 
@@ -148,13 +213,135 @@ class IKController:
148
  target_pos: Target position [x, y, z]
149
  target_quat: Target orientation quaternion [w, x, y, z] (optional)
150
  current_qpos: Starting joint positions (uses current if None)
 
 
151
 
152
  Returns:
153
  Joint positions to reach target
154
  """
155
- # For now, just use position-only IK
156
- # Full 6-DOF IK would need to include orientation in the error
157
- return self.compute_ik(target_pos, current_qpos)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  def reset(self):
160
  """Reset controller state."""
 
73
  """Get current end-effector position."""
74
  return self.data.site_xpos[self.ee_site_id].copy()
75
 
76
+ def get_ee_orientation(self):
77
+ """Get current end-effector orientation as rotation matrix."""
78
+ return self.data.site_xmat[self.ee_site_id].reshape(3, 3).copy()
79
+
80
+ def get_ee_quat(self):
81
+ """Get current end-effector orientation as quaternion [w, x, y, z]."""
82
+ xmat = self.get_ee_orientation()
83
+ quat = np.zeros(4)
84
+ mujoco.mju_mat2Quat(quat, xmat.flatten())
85
+ return quat
86
+
87
+ def compute_jacobian(self, include_rotation=False):
88
+ """Compute the Jacobian for the end-effector site.
89
+
90
+ Args:
91
+ include_rotation: If True, return full 6x6 Jacobian (position + rotation)
92
+
93
+ Returns:
94
+ If include_rotation: 6xN Jacobian (position rows, then rotation rows)
95
+ Otherwise: 3xN position-only Jacobian
96
+ """
97
  # Zero out arrays
98
  self.jacp.fill(0)
99
  self.jacr.fill(0)
 
101
  # Compute Jacobian using MuJoCo
102
  mujoco.mj_jacSite(self.model, self.data, self.jacp, self.jacr, self.ee_site_id)
103
 
104
+ if include_rotation:
105
+ # Full 6-DOF Jacobian
106
+ jac = np.zeros((6, self.num_joints))
107
+ for i, dof_id in enumerate(self.dof_ids):
108
+ jac[:3, i] = self.jacp[:, dof_id] # Position
109
+ jac[3:, i] = self.jacr[:, dof_id] # Rotation
110
+ return jac
111
+ else:
112
+ # Position-only Jacobian
113
+ jac = np.zeros((3, self.num_joints))
114
+ for i, dof_id in enumerate(self.dof_ids):
115
+ jac[:, i] = self.jacp[:, dof_id]
116
+ return jac
117
 
118
  def compute_ik(self, target_pos, current_qpos=None):
119
  """
 
166
 
167
  return qpos
168
 
169
+ def quat_error(self, quat_current, quat_target):
170
+ """Compute orientation error as axis-angle representation.
171
+
172
+ Args:
173
+ quat_current: Current quaternion [w, x, y, z]
174
+ quat_target: Target quaternion [w, x, y, z]
175
+
176
+ Returns:
177
+ Orientation error as 3D vector (axis * angle)
178
+ """
179
+ # Compute quaternion difference: q_err = q_target * q_current^(-1)
180
+ # For unit quaternions, inverse is conjugate: [w, -x, -y, -z]
181
+ qc = quat_current
182
+ qt = quat_target
183
+
184
+ # Quaternion conjugate of current
185
+ qc_conj = np.array([qc[0], -qc[1], -qc[2], -qc[3]])
186
+
187
+ # Quaternion multiplication: q_err = qt * qc_conj
188
+ q_err = np.zeros(4)
189
+ q_err[0] = qt[0]*qc_conj[0] - qt[1]*qc_conj[1] - qt[2]*qc_conj[2] - qt[3]*qc_conj[3]
190
+ q_err[1] = qt[0]*qc_conj[1] + qt[1]*qc_conj[0] + qt[2]*qc_conj[3] - qt[3]*qc_conj[2]
191
+ q_err[2] = qt[0]*qc_conj[2] - qt[1]*qc_conj[3] + qt[2]*qc_conj[0] + qt[3]*qc_conj[1]
192
+ q_err[3] = qt[0]*qc_conj[3] + qt[1]*qc_conj[2] - qt[2]*qc_conj[1] + qt[3]*qc_conj[0]
193
+
194
+ # Ensure we take the shortest path (w >= 0)
195
+ if q_err[0] < 0:
196
+ q_err = -q_err
197
+
198
+ # Convert to axis-angle: angle = 2 * acos(w), axis = [x,y,z] / sin(angle/2)
199
+ # For small angles, error ≈ 2 * [x, y, z]
200
+ angle = 2.0 * np.arccos(np.clip(q_err[0], -1.0, 1.0))
201
+ if angle < 1e-6:
202
+ return np.zeros(3)
203
+
204
+ axis = q_err[1:4] / np.sin(angle / 2.0)
205
+ return axis * angle
206
+
207
+ def compute_ik_with_orientation(self, target_pos, target_quat=None, current_qpos=None,
208
+ position_weight=1.0, orientation_weight=0.5):
209
  """
210
  Compute joint positions to reach target pose (position + orientation).
211
 
 
213
  target_pos: Target position [x, y, z]
214
  target_quat: Target orientation quaternion [w, x, y, z] (optional)
215
  current_qpos: Starting joint positions (uses current if None)
216
+ position_weight: Weight for position error (default 1.0)
217
+ orientation_weight: Weight for orientation error (default 0.5)
218
 
219
  Returns:
220
  Joint positions to reach target
221
  """
222
+ # If no orientation specified, fall back to position-only IK
223
+ if target_quat is None:
224
+ return self.compute_ik(target_pos, current_qpos)
225
+
226
+ if current_qpos is None:
227
+ qpos = self.get_current_joint_positions()
228
+ else:
229
+ qpos = current_qpos.copy()
230
+
231
+ target_p = np.array(target_pos)
232
+ target_q = np.array(target_quat)
233
+
234
+ # Normalize target quaternion
235
+ target_q = target_q / np.linalg.norm(target_q)
236
+
237
+ # Tolerance for orientation (radians)
238
+ orientation_tolerance = 0.01
239
+
240
+ for _ in range(self.max_iterations):
241
+ # Update forward kinematics
242
+ for i, jid in enumerate(self.joint_ids):
243
+ self.data.qpos[self.model.jnt_qposadr[jid]] = qpos[i]
244
+ mujoco.mj_forward(self.model, self.data)
245
+
246
+ # Get current end-effector pose
247
+ ee_pos = self.get_ee_position()
248
+ ee_quat = self.get_ee_quat()
249
+
250
+ # Compute position error
251
+ pos_error = target_p - ee_pos
252
+ pos_error_norm = np.linalg.norm(pos_error)
253
+
254
+ # Compute orientation error
255
+ ori_error = self.quat_error(ee_quat, target_q)
256
+ ori_error_norm = np.linalg.norm(ori_error)
257
+
258
+ # Check convergence
259
+ if pos_error_norm < self.tolerance and ori_error_norm < orientation_tolerance:
260
+ break
261
+
262
+ # Compute full 6-DOF Jacobian
263
+ jac = self.compute_jacobian(include_rotation=True)
264
+
265
+ # Construct weighted error vector [position; orientation]
266
+ error = np.concatenate([
267
+ position_weight * pos_error,
268
+ orientation_weight * ori_error
269
+ ])
270
+
271
+ # Apply weights to Jacobian rows
272
+ jac_weighted = jac.copy()
273
+ jac_weighted[:3, :] *= position_weight
274
+ jac_weighted[3:, :] *= orientation_weight
275
+
276
+ # Damped least squares: dq = J^T * (J * J^T + lambda^2 * I)^-1 * error
277
+ jjt = jac_weighted @ jac_weighted.T
278
+ damped = jjt + self.damping**2 * np.eye(6)
279
+ dq = jac_weighted.T @ np.linalg.solve(damped, error)
280
+
281
+ # Apply step
282
+ qpos = qpos + self.step_size * dq
283
+
284
+ # Clamp to joint limits
285
+ qpos = np.clip(qpos, self.joint_limits_low, self.joint_limits_high)
286
+
287
+ return qpos
288
+
289
+ @staticmethod
290
+ def euler_to_quat(roll, pitch, yaw):
291
+ """Convert Euler angles (XYZ convention) to quaternion [w, x, y, z].
292
+
293
+ Args:
294
+ roll: Rotation around X axis (radians)
295
+ pitch: Rotation around Y axis (radians)
296
+ yaw: Rotation around Z axis (radians)
297
+
298
+ Returns:
299
+ Quaternion [w, x, y, z]
300
+ """
301
+ cy = np.cos(yaw * 0.5)
302
+ sy = np.sin(yaw * 0.5)
303
+ cp = np.cos(pitch * 0.5)
304
+ sp = np.sin(pitch * 0.5)
305
+ cr = np.cos(roll * 0.5)
306
+ sr = np.sin(roll * 0.5)
307
+
308
+ w = cr * cp * cy + sr * sp * sy
309
+ x = sr * cp * cy - cr * sp * sy
310
+ y = cr * sp * cy + sr * cp * sy
311
+ z = cr * cp * sy - sr * sp * cy
312
+
313
+ return np.array([w, x, y, z])
314
+
315
+ @staticmethod
316
+ def quat_to_euler(quat):
317
+ """Convert quaternion [w, x, y, z] to Euler angles (XYZ convention).
318
+
319
+ Args:
320
+ quat: Quaternion [w, x, y, z]
321
+
322
+ Returns:
323
+ (roll, pitch, yaw) in radians
324
+ """
325
+ w, x, y, z = quat
326
+
327
+ # Roll (x-axis rotation)
328
+ sinr_cosp = 2 * (w * x + y * z)
329
+ cosr_cosp = 1 - 2 * (x * x + y * y)
330
+ roll = np.arctan2(sinr_cosp, cosr_cosp)
331
+
332
+ # Pitch (y-axis rotation)
333
+ sinp = 2 * (w * y - z * x)
334
+ if abs(sinp) >= 1:
335
+ pitch = np.copysign(np.pi / 2, sinp) # Gimbal lock
336
+ else:
337
+ pitch = np.arcsin(sinp)
338
+
339
+ # Yaw (z-axis rotation)
340
+ siny_cosp = 2 * (w * z + x * y)
341
+ cosy_cosp = 1 - 2 * (y * y + z * z)
342
+ yaw = np.arctan2(siny_cosp, cosy_cosp)
343
+
344
+ return roll, pitch, yaw
345
 
346
  def reset(self):
347
  """Reset controller state."""
robots/ur5/model/scene.xml CHANGED
@@ -3,25 +3,36 @@
3
 
4
  <option integrator="implicitfast" cone="elliptic" impratio="10"/>
5
 
 
 
 
 
 
 
6
  <visual>
7
- <headlight diffuse="0.6 0.6 0.6" ambient="0.3 0.3 0.3" specular="0 0 0"/>
8
- <rgba haze="0.15 0.25 0.35 1"/>
9
  <global azimuth="120" elevation="-20"/>
10
  </visual>
11
 
12
  <asset>
13
- <texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072"/>
14
- <texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3"
15
- markrgb="0.8 0.8 0.8" width="300" height="300"/>
16
- <material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2"/>
17
- <material name="table" rgba="0.6 0.5 0.4 1" specular="0.3" shininess="0.3"/>
18
- <material name="target_mat" rgba="0.2 0.8 0.2 0.5" specular="0.5" shininess="0.5"/>
 
 
 
 
19
 
20
- <!-- UR5e materials -->
21
- <material name="black" rgba="0.033 0.033 0.033 1" specular="0.5" shininess="0.25"/>
22
- <material name="jointgray" rgba="0.278 0.278 0.278 1" specular="0.5" shininess="0.25"/>
23
- <material name="linkgray" rgba="0.82 0.82 0.82 1" specular="0.5" shininess="0.25"/>
24
- <material name="urblue" rgba="0.49 0.678 0.8 1" specular="0.5" shininess="0.25"/>
 
25
 
26
  <!-- Gripper materials -->
27
  <material name="metal" rgba="0.58 0.58 0.58 1"/>
@@ -292,10 +303,10 @@
292
  </body>
293
  </body>
294
 
295
- <!-- Optional: object to manipulate -->
296
  <body name="box" pos="0.5 0.2 0.45">
297
  <freejoint name="box_joint"/>
298
- <geom name="box_geom" type="box" size="0.025 0.025 0.025" rgba="1 0.3 0.3 1" mass="0.1"/>
299
  </body>
300
  </worldbody>
301
 
@@ -334,7 +345,7 @@
334
  </actuator>
335
 
336
  <keyframe>
337
- <key name="home" qpos="-1.5708 -1.5708 1.5708 -1.5708 -1.5708 0 0 0 0 0 0 0 0 0 0.5 0.2 0.45 1 0 0 0"
338
- ctrl="-1.5708 -1.5708 1.5708 -1.5708 -1.5708 0 128"/>
339
  </keyframe>
340
  </mujoco>
 
3
 
4
  <option integrator="implicitfast" cone="elliptic" impratio="10"/>
5
 
6
+ <!-- Wandelbots Corporate Design Colors:
7
+ Primary Dark: #01040f (0.004, 0.016, 0.059)
8
+ Light/Secondary: #bcbeec (0.737, 0.745, 0.925)
9
+ Accent: #211c44 (0.129, 0.110, 0.267)
10
+ Highlight: #8b7fef (0.545, 0.498, 0.937)
11
+ -->
12
  <visual>
13
+ <headlight diffuse="0.6 0.6 0.6" ambient="0.35 0.35 0.4" specular="0 0 0"/>
14
+ <rgba haze="0.02 0.04 0.12 1"/>
15
  <global azimuth="120" elevation="-20"/>
16
  </visual>
17
 
18
  <asset>
19
+ <!-- Wandelbots gradient skybox - deep purple to near black -->
20
+ <texture type="skybox" builtin="gradient" rgb1="0.13 0.11 0.27" rgb2="0.004 0.016 0.059" width="512" height="3072"/>
21
+ <!-- Ground with Wandelbots purple accent -->
22
+ <texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.08 0.07 0.15" rgb2="0.04 0.04 0.08"
23
+ markrgb="0.55 0.5 0.94" width="300" height="300"/>
24
+ <material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.15"/>
25
+ <!-- Table with subtle purple tint -->
26
+ <material name="table" rgba="0.18 0.16 0.25 1" specular="0.4" shininess="0.4"/>
27
+ <!-- Target marker with Wandelbots highlight color -->
28
+ <material name="target_mat" rgba="0.55 0.5 0.94 0.6" specular="0.5" shininess="0.5"/>
29
 
30
+ <!-- UR5e materials - with Wandelbots accent colors -->
31
+ <material name="black" rgba="0.02 0.02 0.04 1" specular="0.5" shininess="0.25"/>
32
+ <material name="jointgray" rgba="0.22 0.22 0.26 1" specular="0.5" shininess="0.25"/>
33
+ <material name="linkgray" rgba="0.74 0.75 0.82 1" specular="0.5" shininess="0.25"/>
34
+ <!-- Wandelbots purple accent instead of UR blue -->
35
+ <material name="urblue" rgba="0.55 0.5 0.94 1" specular="0.6" shininess="0.35"/>
36
 
37
  <!-- Gripper materials -->
38
  <material name="metal" rgba="0.58 0.58 0.58 1"/>
 
303
  </body>
304
  </body>
305
 
306
+ <!-- Optional: object to manipulate - Wandelbots lavender accent -->
307
  <body name="box" pos="0.5 0.2 0.45">
308
  <freejoint name="box_joint"/>
309
+ <geom name="box_geom" type="box" size="0.025 0.025 0.025" rgba="0.74 0.75 0.93 1" mass="0.1"/>
310
  </body>
311
  </worldbody>
312
 
 
345
  </actuator>
346
 
347
  <keyframe>
348
+ <key name="home" qpos="0 -2.0 2.0 -1.5708 -1.5708 0 0 0 0 0 0 0 0 0 0.5 0.2 0.45 1 0 0 0"
349
+ ctrl="0 -2.0 2.0 -1.5708 -1.5708 0 128"/>
350
  </keyframe>
351
  </mujoco>
robots/ur5/ur5_env.py CHANGED
@@ -32,14 +32,15 @@ class UR5Env(gym.Env):
32
  "wrist_3_joint",
33
  ]
34
 
35
- # Home position (arm pointing up and forward)
 
36
  DEFAULT_HOME_POSE = np.array([
37
- -1.5708, # shoulder_pan
38
- -1.5708, # shoulder_lift
39
- 1.5708, # elbow
40
- -1.5708, # wrist_1
41
- -1.5708, # wrist_2
42
- 0.0, # wrist_3
43
  ], dtype=np.float32)
44
 
45
  def __init__(self, render_mode=None, width=1280, height=720):
@@ -109,8 +110,13 @@ class UR5Env(gym.Env):
109
  arm_joint_names=self.JOINT_NAMES
110
  )
111
 
112
- # Target position for IK
113
- self._target_pos = np.array([0.4, 0.0, 0.6], dtype=np.float32)
 
 
 
 
 
114
  # Gripper target (0=closed, 255=open)
115
  self._gripper_target = 128.0
116
 
@@ -129,6 +135,22 @@ class UR5Env(gym.Env):
129
  """Get current target position."""
130
  return self._target_pos.copy()
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  def set_gripper(self, value: float):
133
  """Set gripper target (0=closed, 255=open)."""
134
  self._gripper_target = np.clip(value, 0, 255)
@@ -229,7 +251,8 @@ class UR5Env(gym.Env):
229
  self.data.qpos[box_qpos_start+3:box_qpos_start+7] = [1, 0, 0, 0]
230
 
231
  # Reset target
232
- self._target_pos = np.array([0.4, 0.0, 0.6], dtype=np.float32)
 
233
  self.data.mocap_pos[0] = self._target_pos
234
  self._gripper_target = 128.0
235
 
@@ -270,8 +293,19 @@ class UR5Env(gym.Env):
270
  def step_with_controller(self, dt: float = 0.002):
271
  """Step using controller (IK or direct joint control)."""
272
  if self._control_mode == 'ik':
273
- # Compute IK to reach target position
274
- joint_targets = self.controller.compute_ik(self._target_pos)
 
 
 
 
 
 
 
 
 
 
 
275
  else:
276
  # Use direct joint targets
277
  joint_targets = self._joint_targets
 
32
  "wrist_3_joint",
33
  ]
34
 
35
+ # Home position - arm reaching forward with gripper pointing down
36
+ # This is a typical "ready to pick" pose for manipulation tasks
37
  DEFAULT_HOME_POSE = np.array([
38
+ 0.0, # shoulder_pan - facing forward
39
+ -2.0, # shoulder_lift - angled down
40
+ 2.0, # elbow - bent
41
+ -1.57, # wrist_1 - aligned
42
+ -1.57, # wrist_2 - gripper pointing down
43
+ 0.0, # wrist_3 - no rotation
44
  ], dtype=np.float32)
45
 
46
  def __init__(self, render_mode=None, width=1280, height=720):
 
110
  arm_joint_names=self.JOINT_NAMES
111
  )
112
 
113
+ # Target position for IK - matching the new home pose
114
+ self._target_pos = np.array([0.4, 0.0, 0.4], dtype=np.float32)
115
+ # Target orientation as Euler angles (roll, pitch, yaw) in radians
116
+ # Default: gripper pointing down (-pi/2 pitch)
117
+ self._target_euler = np.array([0.0, np.pi/2, 0.0], dtype=np.float32)
118
+ # Whether to use orientation in IK (user can toggle this)
119
+ self._use_orientation = True
120
  # Gripper target (0=closed, 255=open)
121
  self._gripper_target = 128.0
122
 
 
135
  """Get current target position."""
136
  return self._target_pos.copy()
137
 
138
+ def set_target_orientation(self, roll: float, pitch: float, yaw: float):
139
+ """Set target orientation as Euler angles (radians)."""
140
+ self._target_euler = np.array([roll, pitch, yaw], dtype=np.float32)
141
+
142
+ def get_target_orientation(self):
143
+ """Get target orientation as Euler angles (radians)."""
144
+ return self._target_euler.copy()
145
+
146
+ def set_use_orientation(self, use: bool):
147
+ """Enable/disable orientation control in IK mode."""
148
+ self._use_orientation = use
149
+
150
+ def get_use_orientation(self):
151
+ """Check if orientation control is enabled."""
152
+ return self._use_orientation
153
+
154
  def set_gripper(self, value: float):
155
  """Set gripper target (0=closed, 255=open)."""
156
  self._gripper_target = np.clip(value, 0, 255)
 
251
  self.data.qpos[box_qpos_start+3:box_qpos_start+7] = [1, 0, 0, 0]
252
 
253
  # Reset target
254
+ self._target_pos = np.array([0.4, 0.0, 0.4], dtype=np.float32)
255
+ self._target_euler = np.array([0.0, np.pi/2, 0.0], dtype=np.float32) # Gripper pointing down
256
  self.data.mocap_pos[0] = self._target_pos
257
  self._gripper_target = 128.0
258
 
 
293
  def step_with_controller(self, dt: float = 0.002):
294
  """Step using controller (IK or direct joint control)."""
295
  if self._control_mode == 'ik':
296
+ if self._use_orientation:
297
+ # Compute IK with position and orientation
298
+ target_quat = IKController.euler_to_quat(
299
+ self._target_euler[0],
300
+ self._target_euler[1],
301
+ self._target_euler[2]
302
+ )
303
+ joint_targets = self.controller.compute_ik_with_orientation(
304
+ self._target_pos, target_quat
305
+ )
306
+ else:
307
+ # Compute IK with position only
308
+ joint_targets = self.controller.compute_ik(self._target_pos)
309
  else:
310
  # Use direct joint targets
311
  joint_targets = self._joint_targets