1- #!/usr/bin/env python3
2- """Test IK with reachable positions."""
1+ #!/usr/bin/env python
2+ """
3+ OpenArm Wave Demo - Right arm waves by rotating wrist (J6)
34
4- import numpy as np
5- from openarm .kinematics .inverse .ikpy import IkpyInverseKinematics
6-
7- print ("=== Testing Reachable Positions ===\n " )
5+ Usage:
6+ python wave_demo.py # Default: 20 waves, 30°, 60°/s
7+ python wave_demo.py --waves 10 # 10 waves
8+ python wave_demo.py --angle 40 --speed 90 # Faster, wider waves
9+ """
810
9- ik_solver = IkpyInverseKinematics ()
10-
11- # First, let's see where the arm is at some known joint configurations
12- print ("=== Forward Kinematics Test ===" )
13- print ("Testing where the end-effector is at various joint angles:\n " )
11+ import time
12+ import argparse
13+ import numpy as np
14+ from lerobot .robots .bi_openarm_follower import BiOpenArmFollower
15+ from lerobot .robots .bi_openarm_follower .config_bi_openarm_follower import BiOpenArmFollowerConfig
16+ from lerobot .robots .openarm_follower .config_openarm_follower import OpenArmFollowerConfig
1417
15- test_configs = [
16- ("All zeros" , np .zeros (7 )),
17- ("J1=45°" , np .deg2rad ([45 , 0 , 0 , 0 , 0 , 0 , 0 ])),
18- ("J2=45°" , np .deg2rad ([0 , 45 , 0 , 0 , 0 , 0 , 0 ])),
19- ("J4=90°" , np .deg2rad ([0 , 0 , 0 , 90 , 0 , 0 , 0 ])),
20- ("Home pose" , np .deg2rad ([0 , - 30 , 0 , 60 , 0 , 0 , 0 ])),
21- ]
18+ # Raised position for right arm
19+ RAISED_POSITION = {
20+ 'joint_1' : 13.34 ,
21+ 'joint_2' : 84.02 ,
22+ 'joint_3' : 62.29 ,
23+ 'joint_4' : 90.57 ,
24+ 'joint_5' : - 78.25 ,
25+ 'joint_6' : 0.00 ,
26+ 'joint_7' : 0.00 ,
27+ 'gripper' : 0.00 ,
28+ }
2229
23- reachable_positions = []
30+ # Zero/rest position
31+ ZERO_POSITION = {
32+ 'joint_1' : 0.0 ,
33+ 'joint_2' : 0.0 ,
34+ 'joint_3' : 0.0 ,
35+ 'joint_4' : 0.0 ,
36+ 'joint_5' : 0.0 ,
37+ 'joint_6' : 0.0 ,
38+ 'joint_7' : 0.0 ,
39+ 'gripper' : 0.0 ,
40+ }
2441
25- for name , joint_angles in test_configs :
26- # Build full joint array (12 links total)
27- joint_angles_full = np .zeros (12 )
28- joint_angles_full [1 :8 ] = joint_angles
42+ def move_smooth (robot , arm , start_pos , end_pos , duration = 1.0 , fps = 60 ):
43+ """Move arm smoothly from start to end position."""
44+ num_steps = int (duration * fps )
45+ dt = 1.0 / fps
46+ other_arm = 'left' if arm == 'right' else 'right'
2947
30- # Compute FK
31- fk_matrix = ik_solver ._right_chain .forward_kinematics (joint_angles_full )
32- pos = fk_matrix [:3 , 3 ]
48+ # Get state
49+ state = robot .get_observation ()
3350
34- print (f"{ name :15s} : x={ pos [0 ]:>7.3f} , y={ pos [1 ]:>7.3f} , z={ pos [2 ]:>7.3f} " )
35- reachable_positions .append ((name , pos ))
51+ # Build arrays
52+ start = np .array ([start_pos [f'joint_{ i } ' ] for i in range (1 , 8 )])
53+ start = np .append (start , start_pos ['gripper' ])
54+
55+ end = np .array ([end_pos [f'joint_{ i } ' ] for i in range (1 , 8 )])
56+ end = np .append (end , end_pos ['gripper' ])
57+
58+ for step in range (num_steps + 1 ):
59+ t = step / num_steps
60+ smooth_t = 0.5 - 0.5 * np .cos (t * np .pi )
61+ pos = start + smooth_t * (end - start )
62+
63+ action = {}
64+ # Keep other arm still
65+ for i in range (1 , 8 ):
66+ action [f"{ other_arm } _joint_{ i } .pos" ] = state [f"{ other_arm } _joint_{ i } .pos" ]
67+ action [f"{ other_arm } _gripper.pos" ] = state [f"{ other_arm } _gripper.pos" ]
68+
69+ # Move active arm
70+ for i in range (7 ):
71+ action [f"{ arm } _joint_{ i + 1 } .pos" ] = float (pos [i ])
72+ action [f"{ arm } _gripper.pos" ] = float (pos [7 ])
73+
74+ robot .send_action (action )
75+ time .sleep (dt )
3676
37- print ("\n === Inverse Kinematics Test ===" )
38- print ("Now testing IK to reach those same positions:\n " )
77+ def wave_j6 (robot , base_position , wave_angle = 30.0 , num_waves = 20 , speed = 60 ):
78+ """
79+ Wave by oscillating J6 (wrist pitch).
80+
81+ Args:
82+ robot: BiOpenArmFollower instance
83+ base_position: Base position dict
84+ wave_angle: Max angle to wave (degrees)
85+ num_waves: Number of complete waves
86+ speed: Wave speed in degrees/second
87+ """
88+ # Calculate duration for one half-wave
89+ half_wave_duration = wave_angle / speed
90+
91+ # Create wave positions
92+ wave_left = base_position .copy ()
93+ wave_left ['joint_6' ] = - wave_angle
94+
95+ wave_right = base_position .copy ()
96+ wave_right ['joint_6' ] = wave_angle
97+
98+ print (f"Waving J6 between -{ wave_angle } ° and +{ wave_angle } ° ({ num_waves } waves)..." )
99+
100+ for i in range (num_waves ):
101+ # Wave left
102+ move_smooth (robot , 'right' , base_position if i == 0 else wave_right , wave_left , duration = half_wave_duration )
103+ # Wave right
104+ move_smooth (robot , 'right' , wave_left , wave_right , duration = half_wave_duration )
105+
106+ if (i + 1 ) % 5 == 0 :
107+ print (f" Completed { i + 1 } /{ num_waves } waves" )
108+
109+ # Return to center
110+ move_smooth (robot , 'right' , wave_right , base_position , duration = half_wave_duration )
39111
40- for name , target_pos in reachable_positions :
41- print (f"Target: { name } -> x={ target_pos [0 ]:.3f} , y={ target_pos [1 ]:.3f} , z={ target_pos [2 ]:.3f} " )
42-
43- # Solve IK
44- joint_solution = ik_solver .solve_right_arm (target_pos )
45- joint_solution_deg = np .rad2deg (joint_solution )
46-
47- # Check if solution is valid (not all zeros)
48- if np .allclose (joint_solution , 0 , atol = 1e-3 ):
49- print (" ✗ IK FAILED (returned zeros)" )
50- else :
51- print (f" ✓ IK SUCCESS: { joint_solution_deg } " )
112+ def main (num_waves = 20 , wave_angle = 30.0 , speed = 60 ):
113+ """Run wave demo with specified parameters."""
114+ print ("=" * 60 )
115+ print ("OpenArm Simple Wave Demo - Right Arm J6 Wave" )
116+ print ("=" * 60 )
117+ print (f"Parameters: { num_waves } waves, ±{ wave_angle } °, { speed } °/s" )
118+ print ("=" * 60 )
119+
120+ # Create robot configuration
121+ left_config = OpenArmFollowerConfig (port = 'can1' , side = 'left' )
122+ right_config = OpenArmFollowerConfig (port = 'can0' , side = 'right' )
123+ config = BiOpenArmFollowerConfig (
124+ left_arm_config = left_config ,
125+ right_arm_config = right_config
126+ )
127+
128+ # Initialize robot
129+ print ("\n Connecting to robot..." )
130+ robot = BiOpenArmFollower (config )
131+ robot .connect ()
132+ print ("✓ Robot connected" )
133+
134+ try :
135+ # Simple wave demo
136+ print ("\n [1/3] Raising right arm to wave position..." )
137+ state = robot .get_observation ()
138+ current_pos = {f'joint_{ i } ' : state [f"right_joint_{ i } .pos" ] for i in range (1 , 8 )}
139+ current_pos ['gripper' ] = state ['right_gripper.pos' ]
140+
141+ move_smooth (robot , 'right' , current_pos , RAISED_POSITION , duration = 3.0 )
142+ print ("✓ Right arm raised" )
143+ time .sleep (0.5 )
144+
145+ print (f"\n [2/3] Waving { num_waves } times at { speed } °/s..." )
146+ wave_j6 (robot , RAISED_POSITION , wave_angle = wave_angle , num_waves = num_waves , speed = speed )
147+ print ("✓ Wave complete" )
148+ time .sleep (0.5 )
52149
53- # Verify with FK
54- joint_full = np .zeros (12 )
55- joint_full [1 :8 ] = joint_solution
56- fk_check = ik_solver ._right_chain .forward_kinematics (joint_full )
57- pos_check = fk_check [:3 , 3 ]
58- error = np .linalg .norm (pos_check - target_pos )
59- print (f" Verification: x={ pos_check [0 ]:.3f} , y={ pos_check [1 ]:.3f} , z={ pos_check [2 ]:.3f} " )
60- print (f" Error: { error * 1000 :.2f} mm" )
61- print ()
150+ print ("\n [3/3] Returning to zero position..." )
151+ move_smooth (robot , 'right' , RAISED_POSITION , ZERO_POSITION , duration = 3.0 )
152+ print ("✓ Returned to zero" )
153+
154+ print ("\n " + "=" * 60 )
155+ print ("✓ Demo complete!" )
156+ print ("=" * 60 )
157+
158+ except KeyboardInterrupt :
159+ print ("\n \n Demo interrupted by user" )
160+
161+ finally :
162+ print ("\n Disconnecting robot..." )
163+ robot .disconnect ()
164+ print ("✓ Robot disconnected" )
62165
63- print ("\n === Workspace Analysis ===" )
64- print ("Based on the FK results, the right arm workspace appears to be:" )
65- all_positions = np .array ([pos for _ , pos in reachable_positions ])
66- print (f" X range: { all_positions [:, 0 ].min ():.3f} to { all_positions [:, 0 ].max ():.3f} " )
67- print (f" Y range: { all_positions [:, 1 ].min ():.3f} to { all_positions [:, 1 ].max ():.3f} " )
68- print (f" Z range: { all_positions [:, 2 ].min ():.3f} to { all_positions [:, 2 ].max ():.3f} " )
166+ if __name__ == "__main__" :
167+ parser = argparse .ArgumentParser (
168+ description = 'OpenArm wave demo - makes right arm wave using J6' ,
169+ formatter_class = argparse .RawDescriptionHelpFormatter ,
170+ epilog = """
171+ Examples:
172+ python wave_demo.py # Default: 20 waves, 30°, 60°/s
173+ python wave_demo.py --waves 10 # Quick demo with 10 waves
174+ python wave_demo.py --angle 40 --speed 90 # Faster, wider waves
175+ """
176+ )
177+
178+ parser .add_argument ('--waves' , type = int , default = 5 , help = 'Number of complete waves (default: 5)' )
179+ parser .add_argument ('--angle' , type = float , default = 30.0 , help = 'Wave angle in degrees (default: 30.0)' )
180+ parser .add_argument ('--speed' , type = float , default = 30.0 , help = 'Wave speed in degrees/second (default: 30.0)' )
181+
182+ args = parser .parse_args ()
183+
184+ # Validate parameters
185+ if args .waves < 1 :
186+ parser .error ("--waves must be at least 1" )
187+ if args .angle <= 0 or args .angle > 40 :
188+ parser .error ("--angle must be between 0 and 40 degrees (J6 limit is ±40°)" )
189+ if args .speed <= 0 or args .speed > 120 :
190+ parser .error ("--speed must be between 0 and 120 degrees/second" )
191+
192+ main (num_waves = args .waves , wave_angle = args .angle , speed = args .speed )
0 commit comments