Skip to content

Add leader-follower mode for SO101#314

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

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

Conversation

@SinedYuk
Copy link
Copy Markdown
Contributor

@SinedYuk SinedYuk commented Feb 23, 2026

Add leader-follower teleoperation for SO101

Adds passive mode to the SO101 driver and a new main_leader_follower data collection entry point. Two physical SO101 arms connect via pimm signals: the leader (torques disabled) is moved by hand, and its joint positions are forwarded as JointPosition commands to the follower in real-time.

Changes

positronic/drivers/roboarm/so101/driver.py

  • Add passive parameter to Robot — disables torque, skips command processing, runs at 100 Hz (vs 1000 Hz for active)
  • commands and target_grip receivers are only created when passive=False
  • Move Kinematics initialization into run() (placo RobotWrapper is not picklable)
  • Extract _solve_ik, _forward_kinematics, _norm_to_rad, _rad_to_norm as module-level functions

positronic/data_collection.py

  • Add main_leader_follower() — wires leader → follower via pimm.map, records dataset with keyboard controls (s = start, p = stop, q = quit)
  • Add so101_leader CLI config with so101_passive leader arm
  • Deduplicate bg_cs list in main() (robot_arm and gripper are the same object for SO101)

positronic/cfg/hardware/roboarm/__init__.py

  • Add so101_passive

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Deleted the command

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added passive mode, reused keyboard control class, and extracted data collection logic into data_collection

@SinedYuk SinedYuk requested a review from vertix April 10, 2026 14:18
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