Skip to content

Commit dd515c4

Browse files
Pruthvi-Neuracoremark-neuracore
authored andcommitted
feat: add foot pedal control for neuracore recording
1 parent 01315d7 commit dd515c4

5 files changed

Lines changed: 629 additions & 0 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#!/usr/bin/env python3
2+
"""AgileX PiPER Teleoperation using LeRobot Leader Arm and Foot Pedal.
3+
4+
This script maps a physical LeRobot leader arm and a 3-button Foot Pedal
5+
to a simulated AgileX PiPER robot in Neuracore.
6+
"""
7+
8+
import argparse
9+
import os
10+
import sys
11+
import threading
12+
import time
13+
import traceback
14+
from pathlib import Path
15+
16+
import numpy as np
17+
18+
# Resolve workspace paths
19+
_file_path = Path(__file__).resolve()
20+
_example_agilex_root = _file_path.parent.parent
21+
_ws_root = _example_agilex_root.parent
22+
23+
# Add example_so101 to path for the leader_arm module
24+
_example_so101_root = _ws_root / "example_so101"
25+
sys.path.insert(0, str(_example_so101_root))
26+
sys.path.insert(0, str(_example_so101_root / "examples"))
27+
28+
# Add neuracore and other dependencies
29+
_neuracore_pkg_root = _ws_root / "neuracore"
30+
sys.path.insert(0, str(_neuracore_pkg_root))
31+
32+
# 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", "")
34+
35+
import neuracore as nc
36+
37+
# 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
41+
42+
# AgileX PiPER Configurations
43+
PIPER_JOINT_NAMES = ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"]
44+
# 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)
49+
50+
PIPER_OFFSETS_DEG = np.zeros(6, dtype=np.float64)
51+
PIPER_DIRECTIONS = np.ones(6, dtype=np.float64)
52+
53+
# Leader (5 joints) to AgileX (6 joints) mapping
54+
# Leader 0 (Pan) -> Piper joint1
55+
# Leader 1 (Lift) -> Piper joint2
56+
# Leader 2 (Elbow) -> Piper joint3
57+
# Leader 3 (Wrist Flex) -> Piper joint5
58+
# Leader 4 (Wrist Roll) -> Piper joint6
59+
LEADER_TO_PIPER_JOINT = {0: 0, 1: 1, 2: 2, 3: 4, 4: 5}
60+
PIPER_FIXED_JOINTS = {3: 0.0} # joint4 fixed at 0
61+
62+
URDF_PATH = _example_agilex_root / "piper_description" / "urdf" / "piper_description.urdf"
63+
64+
def leader_reader_thread(data_manager: DataManager, leader: LerobotSO101LeaderArm, rate: float):
65+
dt = 1.0 / rate
66+
while not data_manager.is_shutdown_requested():
67+
try:
68+
if leader.is_connected:
69+
angles, gripper = leader.read_mapped()
70+
data_manager.set_leader_mapped_state(angles, gripper)
71+
except Exception as e:
72+
print(f"Error in leader reader thread: {e}")
73+
time.sleep(dt)
74+
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
83+
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+
87+
args = parser.parse_args()
88+
89+
print("=" * 60)
90+
print("AGILEX PIPER LE ROBOT + FOOT PEDAL TELEOP")
91+
print("=" * 60)
92+
93+
# 1. Initialize Leader Arm
94+
print(f"\n🦾 Connecting to Le Robot leader on {args.leader_port}...")
95+
leader = LerobotSO101LeaderArm(port=args.leader_port, calibration_id=args.leader_id)
96+
try:
97+
leader.connect(calibrate=False)
98+
except Exception as e:
99+
print(f"❌ Failed to connect to leader: {e}")
100+
print("Tip: Run the robust calibration script first.")
101+
sys.exit(1)
102+
103+
# 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
111+
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
116+
local_directions = np.array([1.0, 1.0, -1.0, 1.0, 1.0, 1.0])
117+
118+
leader.configure_follower(
119+
follower_limits_deg=local_limits_deg,
120+
follower_offsets_deg=local_offsets_deg,
121+
follower_directions=local_directions,
122+
leader_to_follower_joint=LEADER_TO_PIPER_JOINT,
123+
fixed_joints=PIPER_FIXED_JOINTS
124+
)
125+
print("✓ Leader arm configured with OFFICIAL offsets")
126+
127+
# Neutral Pose for Parking/Home (degrees) - Using official values
128+
NEUTRAL_POSE_DEG = np.array([-1.0, 80.0, -51.0, 0.0, 16.0, 2.6])
129+
130+
# 2. Initialize Data Manager and Foot Pedal
131+
data_manager = DataManager()
132+
data_manager.set_robot_activity_state(RobotActivityState.DISABLED) # Start DISABLED
133+
134+
pedal = FootPedal()
135+
pedal.save_config(activate_key=args.pedal_activate, home_key=args.pedal_home, record_key=args.pedal_record)
136+
137+
# Define callbacks first
138+
def on_activate():
139+
state = data_manager.get_robot_activity_state()
140+
if state == RobotActivityState.ENABLED:
141+
data_manager.set_robot_activity_state(RobotActivityState.DISABLED)
142+
print("✓ [PEDAL] Robot DISABLED (Parking...)")
143+
else:
144+
data_manager.set_robot_activity_state(RobotActivityState.ENABLED)
145+
print("✓ [PEDAL] Robot ENABLED (Following Leader)")
146+
147+
def on_home():
148+
print("🏠 [PEDAL] Moving to home position...")
149+
data_manager.set_robot_activity_state(RobotActivityState.HOMING)
150+
151+
def on_record():
152+
print("⏺️ [PEDAL] Toggling recording...")
153+
if not nc.is_recording():
154+
try:
155+
nc.start_recording()
156+
print("✓ [PEDAL] Recording STARTED")
157+
except Exception as e:
158+
print(f"✗ [PEDAL] Failed to start recording: {e}")
159+
else:
160+
try:
161+
nc.stop_recording()
162+
print("✓ [PEDAL] Recording STOPPED")
163+
except Exception as e:
164+
print(f"✗ [PEDAL] Failed to stop recording: {e}")
165+
166+
# Explicitly register
167+
pedal.on("activate", on_activate)
168+
pedal.on("home", on_home)
169+
pedal.on("record", on_record)
170+
171+
pedal.start()
172+
print(f"⌨️ Foot Pedal listener started (Keys: {args.pedal_activate}, {args.pedal_home}, {args.pedal_record})")
173+
174+
# 3. Initialize Neuracore
175+
print("\n🔧 Initializing Neuracore...")
176+
nc.login()
177+
print(f"📦 Connecting to Robot: {args.robot_name}")
178+
nc.connect_robot(robot_name=args.robot_name, urdf_path=str(URDF_PATH))
179+
180+
ds_name = f"piper-leader-teleop-{time.strftime('%Y%m%d-%H%M%S')}"
181+
print(f"📂 Creating Dataset: {ds_name}")
182+
nc.create_dataset(name=ds_name)
183+
184+
# 4. Start Leader Reading Thread
185+
reader_thread = threading.Thread(
186+
target=leader_reader_thread,
187+
args=(data_manager, leader, args.leader_rate),
188+
daemon=True
189+
)
190+
reader_thread.start()
191+
192+
print("\n🚀 TELEOP READY")
193+
print("------------------------------------------------------------")
194+
print("1. Robot starts in DISABLED (Neutral) pose.")
195+
print(f"2. Press '{args.pedal_activate}' to ENABLE (Mirror Leader).")
196+
print(f"3. Press '{args.pedal_record}' to Toggle Recording.")
197+
print("------------------------------------------------------------")
198+
199+
loop_count = 0
200+
try:
201+
while True:
202+
t0 = time.time()
203+
mapped_angles, mapped_gripper = data_manager.get_leader_mapped_state()
204+
state = data_manager.get_robot_activity_state()
205+
206+
if state == RobotActivityState.HOMING:
207+
angles_deg = NEUTRAL_POSE_DEG
208+
gripper_val = 0.5
209+
time.sleep(0.5)
210+
data_manager.set_robot_activity_state(RobotActivityState.DISABLED)
211+
elif state == RobotActivityState.DISABLED:
212+
angles_deg = NEUTRAL_POSE_DEG
213+
gripper_val = 0.5
214+
else:
215+
# ENABLED: Follow Leader
216+
if mapped_angles is not None:
217+
angles_deg = mapped_angles
218+
gripper_val = mapped_gripper
219+
else:
220+
angles_deg = NEUTRAL_POSE_DEG
221+
gripper_val = 0.5
222+
223+
# Streaming and Logging
224+
if angles_deg is not None:
225+
loop_count += 1
226+
# Debug every 30 loops (~2Hz)
227+
if loop_count % 30 == 0:
228+
if state == RobotActivityState.DISABLED:
229+
print(f"💡 [REMINDER] Robot is DISABLED. Press '{args.pedal_activate}' to Mirror. [Raw Grip: {gripper_val:.2f}]")
230+
else:
231+
print(f"📡 [MIRRORING] J1-3: {np.round(angles_deg[:3], 1)} | Grip: {gripper_val:.2f}")
232+
233+
# Log to Neuracore
234+
data_dict = {}
235+
for i, deg in enumerate(angles_deg):
236+
val = float(np.radians(deg))
237+
# Explicit small offset for joint4 if 0.0 is problematic
238+
if i == 3 and abs(val) < 0.0001:
239+
val = 0.0001
240+
data_dict[f"joint{i+1}"] = val
241+
242+
# Visual Gripper Joints (joint7, joint8)
243+
if gripper_val is not None:
244+
# joint7: 0 (closed) to 0.035 (open)
245+
# joint8: 0 (closed) to -0.035 (open)
246+
data_dict["joint7"] = float(gripper_val * 0.035)
247+
data_dict["joint8"] = float(-gripper_val * 0.035)
248+
249+
nc.log_joint_positions(data_dict, timestamp=t0)
250+
251+
else:
252+
if loop_count % 60 == 0:
253+
print("⚠️ [WARNING] No leader data received! Is it plugged in?")
254+
255+
time.sleep(max(0, 1.0 / 60.0 - (time.time() - t0)))
256+
257+
except KeyboardInterrupt:
258+
print("\n👋 Teleop stopped by user.")
259+
except Exception as e:
260+
print(f"\n❌ Error: {e}")
261+
traceback.print_exc()
262+
finally:
263+
data_manager.request_shutdown()
264+
pedal.stop()
265+
reader_thread.join(timeout=1.0)
266+
leader.disconnect()
267+
nc.logout()
268+
print("👋 Shutdown complete.")
269+
270+
if __name__ == "__main__":
271+
main()

0 commit comments

Comments
 (0)