Skip to content

Add leader-follower mode for SO101#314

Open
SinedYuk wants to merge 3 commits intoPositronic-Robotics:mainfrom
SinedYuk:so101-leader-mode
Open

Add leader-follower mode for SO101#314
SinedYuk wants to merge 3 commits intoPositronic-Robotics:mainfrom
SinedYuk:so101-leader-mode

Conversation

@SinedYuk
Copy link
Copy Markdown
Contributor

@SinedYuk SinedYuk commented Feb 23, 2026

Add leader-follower mode for SO101

Adds physical leader-follower teleoperation for the SO101 arm, enabling data collection without a VR headset.

What's new

LeaderFollower control system (positronic/drivers/roboarm/so101/leader_follower.py)
Reads joint positions from a passive leader arm at 100 Hz and emits them as NormalizedJointPosition commands to the follower. Recording is toggled via ENTER key with debounce, with sound feedback on start/stop.

NormalizedJointPosition command (positronic/drivers/roboarm/command.py)
New command type for direct normalized [0,1] motor position control, bypassing IK. Fully wired through to_wire/from_wire.

Lazy kinematics init in SO101 Robot (positronic/drivers/roboarm/so101/driver.py)
Kinematics is now initialized in run() inside the subprocess rather than in __init__, since placo.RobotWrapper is not picklable. Also handles NormalizedJointPosition commands.

so101_leader data collection entry point (positronic/data_collection.py)
New main_leader_follower() function and so101_leader config registered in the CLI. Deduplication fix for bg_cs list (relevant when robot_arm and gripper are the same object).

Misc updates:

  • Fixed camera recording with LinuxVideo driver — Recording episodes now works correctly with USB cameras (Arducam, Sonix, etc.)
  • Added Sonix USB camera support — New sonix camera config for USB2.0 CAM1
  • Updated so101_leader default camera — Now uses Sonix camera out of the box

Usage

uv run positronic-data-collection so101_leader --output_dir=~/datasets/so101_runs

@SinedYuk SinedYuk force-pushed the so101-leader-mode branch from d96a014 to b0ab533 Compare March 14, 2026 18:08
@SinedYuk SinedYuk marked this pull request as ready for review March 14, 2026 18:09
@SinedYuk SinedYuk force-pushed the so101-leader-mode branch from b0ab533 to 599b0b7 Compare March 28, 2026 16:42
def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]:
self.motor_bus.connect()
# Initialize kinematics in the subprocess (placo.RobotWrapper is not picklable)
_ = self.kinematic
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very hacky. Find a better way to achieve that. For example make _solve_ik and _forward_kinematics static methods that take kinematics as argument

q_norm = self.rad_to_norm(qpos)
q_with_gripper = np.concatenate([q_norm, [self.target_grip.value]])
self.motor_bus.set_target_position(q_with_gripper)
case roboarm_command.NormalizedJointPosition(qpos):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot afford introducing new commands that eaily. The solution would be to convert normalised position into radians in the leader, and send this command to follower.

In other words, I want you to remove NormalizedJointPosition.

Moreover, I suppose that the leader should emit its state with the real joint values. This is the API that robotic arms use.

self.robot_meta_in: pimm.SignalReceiver = pimm.FakeReceiver(self)

def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]:
from positronic.dataset.ds_writer_agent import DsWriterCommand, DsWriterCommandType
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import this on top level pls. BTW I know that claude has this tendency.

from positronic.drivers.roboarm import command as roboarm_command


class LeaderFollower(pimm.ControlSystem):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QQ – do we really need to a separete ControlSystem. Can we just connect two SO101 arms together, where we will read positions from the first one and send them to the second one?

May be just adding passive=True to the arm driver. There's a question on how to enable data collection things, but it's a data collection script concern, not the driver's concern

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants