11#!/usr/bin/env python3
22"""AgileX PiPER Teleoperation using LeRobot Leader Arm and Foot Pedal.
33
4- This script maps a physical LeRobot leader arm and a 3-button Foot Pedal
4+ This script maps a physical LeRobot leader arm and a 3-button Foot Pedal
55to a simulated AgileX PiPER robot in Neuracore.
66"""
77
2828# Add neuracore and other dependencies
2929_neuracore_pkg_root = _ws_root / "neuracore"
3030sys .path .insert (0 , str (_neuracore_pkg_root ))
31+ sys .path .insert (
32+ 0 , str (_example_agilex_root )
33+ ) # Fix for local piper_controller if needed
3134
3235# CRITICAL: Set PYTHONPATH so that spawned subprocesses (like the data daemon) can find neuracore
33- os .environ ["PYTHONPATH" ] = str (_neuracore_pkg_root ) + os .pathsep + os .environ .get ("PYTHONPATH" , "" )
36+ os .environ ["PYTHONPATH" ] = (
37+ str (_neuracore_pkg_root ) + os .pathsep + os .environ .get ("PYTHONPATH" , "" )
38+ )
3439
35- import neuracore as nc
40+ import neuracore as nc # noqa: E402
3641
3742# Import from example_so101 common components
38- from common .data_manager import DataManager , RobotActivityState
39- from common .leader_arm import LerobotSO101LeaderArm
40- from neuracore .core .input_devices .foot_pedal import FootPedal
43+ from common .data_manager import DataManager , RobotActivityState # noqa: E402
44+ from common .leader_arm import LerobotSO101LeaderArm # noqa: E402
45+ from neuracore .core .input_devices .foot_pedal import FootPedal # noqa: E402
4146
4247# AgileX PiPER Configurations
4348PIPER_JOINT_NAMES = ["joint1" , "joint2" , "joint3" , "joint4" , "joint5" , "joint6" ]
4449# Limits in degrees
45- PIPER_LIMITS_DEG = np .array ([
46- (- 150.0 , 150.0 ), (0.0 , 180.0 ), (- 170.0 , 0.0 ),
47- (- 100.0 , 100.0 ), (- 70.0 , 70.0 ), (- 120.0 , 120.0 )
48- ], dtype = np .float64 )
50+ PIPER_LIMITS_DEG = np .array (
51+ [
52+ (- 150.0 , 150.0 ),
53+ (0.0 , 180.0 ),
54+ (- 170.0 , 0.0 ),
55+ (- 100.0 , 100.0 ),
56+ (- 70.0 , 70.0 ),
57+ (- 120.0 , 120.0 ),
58+ ],
59+ dtype = np .float64 ,
60+ )
4961
5062PIPER_OFFSETS_DEG = np .zeros (6 , dtype = np .float64 )
5163PIPER_DIRECTIONS = np .ones (6 , dtype = np .float64 )
5769# Leader 3 (Wrist Flex) -> Piper joint5
5870# Leader 4 (Wrist Roll) -> Piper joint6
5971LEADER_TO_PIPER_JOINT = {0 : 0 , 1 : 1 , 2 : 2 , 3 : 4 , 4 : 5 }
60- PIPER_FIXED_JOINTS = {3 : 0.0 } # joint4 fixed at 0
72+ PIPER_FIXED_JOINTS = {3 : 0.0 } # joint4 fixed at 0
6173
62- URDF_PATH = _example_agilex_root / "piper_description" / "urdf" / "piper_description.urdf"
74+ URDF_PATH = (
75+ _example_agilex_root / "piper_description" / "urdf" / "piper_description.urdf"
76+ )
6377
64- def leader_reader_thread (data_manager : DataManager , leader : LerobotSO101LeaderArm , rate : float ):
78+
79+ def leader_reader_thread (
80+ data_manager : DataManager , leader : LerobotSO101LeaderArm , rate : float
81+ ) -> None :
82+ """Thread for reading data from the leader arm and updating the shared state.
83+
84+ Args:
85+ data_manager: Shared state manager.
86+ leader: Initialized leader arm instance.
87+ rate: Frequency in Hz to read the arm.
88+ """
6589 dt = 1.0 / rate
6690 while not data_manager .is_shutdown_requested ():
6791 try :
6892 if leader .is_connected :
6993 angles , gripper = leader .read_mapped ()
70- data_manager .set_leader_mapped_state (angles , gripper )
94+ data_manager .set_leader_mapped_state (angles , gripper ) # type: ignore[attr-defined]
7195 except Exception as e :
7296 print (f"Error in leader reader thread: { e } " )
7397 time .sleep (dt )
7498
75- def main ():
76- parser = argparse .ArgumentParser (description = "AgileX PiPER Teleop with LeRobot Leader and Foot Pedal" )
77- parser .add_argument ("--leader-port" , type = str , default = "/dev/ttyACM0" , help = "USB port for leader arm" )
78- parser .add_argument ("--leader-id" , type = str , default = "my_awesome_leader_arm" , help = "LeRobot calibration id" )
79- parser .add_argument ("--leader-rate" , type = float , default = 50.0 , help = "Reading rate in Hz" )
80- parser .add_argument ("--robot-name" , type = str , default = "AgileX PiPER" , help = "Robot name in Neuracore" )
81-
82- # Pedal keys
99+
100+ def main () -> None :
101+ """Main execution entry point for teleoperation script."""
102+ parser = argparse .ArgumentParser (
103+ description = "AgileX PiPER Teleop with LeRobot Leader and Foot Pedal"
104+ )
105+ parser .add_argument (
106+ "--leader-port" ,
107+ type = str ,
108+ default = "/dev/ttyACM0" ,
109+ help = "USB port for leader arm" ,
110+ )
111+ parser .add_argument (
112+ "--leader-id" ,
113+ type = str ,
114+ default = "my_awesome_leader_arm" ,
115+ help = "LeRobot calibration id" ,
116+ )
117+ parser .add_argument (
118+ "--leader-rate" , type = float , default = 50.0 , help = "Reading rate in Hz"
119+ )
120+ parser .add_argument (
121+ "--robot-name" , type = str , default = "AgileX PiPER" , help = "Robot name in Neuracore"
122+ )
123+
124+ # Pedal keys (Matching OpenArm unified mapping)
83125 parser .add_argument ("--pedal-activate" , type = str , default = "a" )
84- parser .add_argument ("--pedal-home" , type = str , default = "b " )
85- parser .add_argument ("--pedal-record" , type = str , default = "c " )
86-
126+ parser .add_argument ("--pedal-home" , type = str , default = "h " )
127+ parser .add_argument ("--pedal-record" , type = str , default = "r " )
128+
87129 args = parser .parse_args ()
88130
89131 print ("=" * 60 )
@@ -99,43 +141,58 @@ def main():
99141 print (f"❌ Failed to connect to leader: { e } " )
100142 print ("Tip: Run the robust calibration script first." )
101143 sys .exit (1 )
102-
144+
103145 # Adjusted Piper Config for better mapping
104- local_limits_deg = np .array ([
105- (- 150.0 , 150.0 ), (0.0 , 160.0 ), (- 160.0 , 0.0 ), # J1, J2, J3
106- (- 100.0 , 100.0 ), (- 100.0 , 100.0 ), (- 180.0 , 180.0 ) # J4, J5, J6
107- ], dtype = np .float64 )
108-
109- # Official Neutral Angles: [-1.0, 80.0, -51.0, -4.0, 16.0, 2.6]
110- # We use these as offsets so leader 0 -> official neutral
146+ local_limits_deg = np .array (
147+ [
148+ (- 150.0 , 150.0 ),
149+ (0.0 , 160.0 ),
150+ (- 160.0 , 0.0 ), # J1, J2, J3
151+ (- 100.0 , 100.0 ),
152+ (- 100.0 , 100.0 ),
153+ (- 180.0 , 180.0 ), # J4, J5, J6
154+ ],
155+ dtype = np .float64 ,
156+ )
157+
158+ # Reverting to the previous working offsets as requested
159+ # These worked well for the user in the initial session
111160 local_offsets_deg = np .array ([- 1.0 , 80.0 , - 51.0 , 0.0 , 16.0 , 2.6 ])
112-
113- # Directions:
114- # J3 (Elbow) often needs to be flipped if leader fold -> piper fold
115- # J2 (Lift) leader up -> piper lift up
116161 local_directions = np .array ([1.0 , 1.0 , - 1.0 , 1.0 , 1.0 , 1.0 ])
117162
118163 leader .configure_follower (
119164 follower_limits_deg = local_limits_deg ,
120165 follower_offsets_deg = local_offsets_deg ,
121166 follower_directions = local_directions ,
122167 leader_to_follower_joint = LEADER_TO_PIPER_JOINT ,
123- fixed_joints = PIPER_FIXED_JOINTS
168+ fixed_joints = PIPER_FIXED_JOINTS ,
124169 )
125- print ("✓ Leader arm configured with OFFICIAL offsets" )
170+ print ("✓ Leader arm configured with WORKING offsets (Reverted) " )
126171
127172 # Neutral Pose for Parking/Home (degrees) - Using official values
128173 NEUTRAL_POSE_DEG = np .array ([- 1.0 , 80.0 , - 51.0 , 0.0 , 16.0 , 2.6 ])
129174
130175 # 2. Initialize Data Manager and Foot Pedal
131176 data_manager = DataManager ()
132- data_manager .set_robot_activity_state (RobotActivityState .DISABLED ) # Start DISABLED
133-
177+ data_manager .set_robot_activity_state (RobotActivityState .DISABLED ) # Start DISABLED
178+
134179 pedal = FootPedal ()
135- pedal .save_config (activate_key = args .pedal_activate , home_key = args .pedal_home , record_key = args .pedal_record )
180+
181+ # HARDCODE working keys for the user's specific FootSwitch
182+ # a = ACTIVATE, b = HOME, c = RECORD
183+ activate_key = "a"
184+ home_key = "b"
185+ record_key = "c"
186+
187+ # Save this config so the FootPedal class picks it up
188+ pedal .save_config (
189+ activate_key = activate_key , home_key = home_key , record_key = record_key
190+ )
191+ print (f"✓ PEDAL MAPPINGS ENFORCED: { pedal .mappings } " )
136192
137193 # Define callbacks first
138- def on_activate ():
194+ def on_activate () -> None :
195+ """Toggle the robot enable/disable state."""
139196 state = data_manager .get_robot_activity_state ()
140197 if state == RobotActivityState .ENABLED :
141198 data_manager .set_robot_activity_state (RobotActivityState .DISABLED )
@@ -144,13 +201,17 @@ def on_activate():
144201 data_manager .set_robot_activity_state (RobotActivityState .ENABLED )
145202 print ("✓ [PEDAL] Robot ENABLED (Following Leader)" )
146203
147- def on_home ():
204+ def on_home () -> None :
205+ """Trigger the homing procedure."""
148206 print ("🏠 [PEDAL] Moving to home position..." )
149207 data_manager .set_robot_activity_state (RobotActivityState .HOMING )
150208
151- def on_record ():
209+ def on_record () -> None :
210+ """Toggle Neuracore recording session."""
152211 print ("⏺️ [PEDAL] Toggling recording..." )
153- if not nc .is_recording ():
212+ current_status = nc .is_recording ()
213+ print (f"🔍 [DEBUG] Current Neuracore recording status: { current_status } " )
214+ if not current_status :
154215 try :
155216 nc .start_recording ()
156217 print ("✓ [PEDAL] Recording STARTED" )
@@ -169,14 +230,18 @@ def on_record():
169230 pedal .on ("record" , on_record )
170231
171232 pedal .start ()
172- print (f"⌨️ Foot Pedal listener started (Keys: { args .pedal_activate } , { args .pedal_home } , { args .pedal_record } )" )
233+ print (
234+ f"⌨️ Foot Pedal listener started (Keys: { args .pedal_activate } , { args .pedal_home } , { args .pedal_record } )"
235+ )
173236
174237 # 3. Initialize Neuracore
175238 print ("\n 🔧 Initializing Neuracore..." )
176239 nc .login ()
177240 print (f"📦 Connecting to Robot: { args .robot_name } " )
178- nc .connect_robot (robot_name = args .robot_name , urdf_path = str (URDF_PATH ))
179-
241+ nc .connect_robot (
242+ robot_name = args .robot_name , urdf_path = str (URDF_PATH ), overwrite = True
243+ )
244+
180245 ds_name = f"piper-leader-teleop-{ time .strftime ('%Y%m%d-%H%M%S' )} "
181246 print (f"📂 Creating Dataset: { ds_name } " )
182247 nc .create_dataset (name = ds_name )
@@ -185,7 +250,7 @@ def on_record():
185250 reader_thread = threading .Thread (
186251 target = leader_reader_thread ,
187252 args = (data_manager , leader , args .leader_rate ),
188- daemon = True
253+ daemon = True ,
189254 )
190255 reader_thread .start ()
191256
@@ -195,14 +260,14 @@ def on_record():
195260 print (f"2. Press '{ args .pedal_activate } ' to ENABLE (Mirror Leader)." )
196261 print (f"3. Press '{ args .pedal_record } ' to Toggle Recording." )
197262 print ("------------------------------------------------------------" )
198-
263+
199264 loop_count = 0
200265 try :
201266 while True :
202267 t0 = time .time ()
203268 mapped_angles , mapped_gripper = data_manager .get_leader_mapped_state ()
204269 state = data_manager .get_robot_activity_state ()
205-
270+
206271 if state == RobotActivityState .HOMING :
207272 angles_deg = NEUTRAL_POSE_DEG
208273 gripper_val = 0.5
@@ -215,7 +280,7 @@ def on_record():
215280 # ENABLED: Follow Leader
216281 if mapped_angles is not None :
217282 angles_deg = mapped_angles
218- gripper_val = mapped_gripper
283+ gripper_val = mapped_gripper if mapped_gripper is not None else 0.5
219284 else :
220285 angles_deg = NEUTRAL_POSE_DEG
221286 gripper_val = 0.5
@@ -226,9 +291,13 @@ def on_record():
226291 # Debug every 30 loops (~2Hz)
227292 if loop_count % 30 == 0 :
228293 if state == RobotActivityState .DISABLED :
229- print (f"💡 [REMINDER] Robot is DISABLED. Press '{ args .pedal_activate } ' to Mirror. [Raw Grip: { gripper_val :.2f} ]" )
294+ print (
295+ f"💡 [REMINDER] Robot is DISABLED. Press '{ args .pedal_activate } ' to Mirror. [Raw Grip: { gripper_val :.2f} ]"
296+ )
230297 else :
231- print (f"📡 [MIRRORING] J1-3: { np .round (angles_deg [:3 ], 1 )} | Grip: { gripper_val :.2f} " )
298+ print (
299+ f"📡 [MIRRORING] J1-3: { np .round (angles_deg [:3 ], 1 )} | Grip: { gripper_val :.2f} "
300+ )
232301
233302 # Log to Neuracore
234303 data_dict = {}
@@ -238,7 +307,7 @@ def on_record():
238307 if i == 3 and abs (val ) < 0.0001 :
239308 val = 0.0001
240309 data_dict [f"joint{ i + 1 } " ] = val
241-
310+
242311 # Visual Gripper Joints (joint7, joint8)
243312 if gripper_val is not None :
244313 # joint7: 0 (closed) to 0.035 (open)
@@ -247,11 +316,11 @@ def on_record():
247316 data_dict ["joint8" ] = float (- gripper_val * 0.035 )
248317
249318 nc .log_joint_positions (data_dict , timestamp = t0 )
250-
319+
251320 else :
252321 if loop_count % 60 == 0 :
253322 print ("⚠️ [WARNING] No leader data received! Is it plugged in?" )
254-
323+
255324 time .sleep (max (0 , 1.0 / 60.0 - (time .time () - t0 )))
256325
257326 except KeyboardInterrupt :
@@ -267,5 +336,6 @@ def on_record():
267336 nc .logout ()
268337 print ("👋 Shutdown complete." )
269338
339+
270340if __name__ == "__main__" :
271341 main ()
0 commit comments