refactor: 插件化重构 — 重命名包、draccus 配置迁移、上下文注册、Pika 复用

1. 重命名 ufactory_lerobot → lerobot_robot_ufactory
   以独立插件包形式支持 lerobot 加载,pyproject.toml 同步更新

2. 配置文件迁移至 draccus 格式
   所有 YAML config 从 _target_ 格式迁移至 ChoiceRegistry type 格式
   dict 值类型从具体类改为抽象基类(RobotConfig/TeleoperatorConfig)
   确保嵌套 dict 中的 type 选择器能被 draccus 正确解析

3. teleop 上下文注册替代属性注入
   新增 context.py(contextvars 实现),支持多 teleop 按 id 存取
   UFBaseTeleop.connect/disconnect 自动注册/注销活跃 teleop
   UFMockRobot.get_observation 从上下文获取 teleop action
   移除 record() 对 cfg.robot.teleop 的注入

4. 部分导入延迟加载
   pika_device: serial/list_ports 延迟导入
   gello_teleop: gello 依赖延迟导入
   umi_teleop/camera_umi: XVLib 延迟导入
   space_mouse: spnav 延迟导入

5. Pika 设备实例复用
   PIKA_DEVICE_MAP 共享已连接的 Sense/Gripper,防止多实例化抢占端口
   修复 pika_gripper 属性误用 _pika_sense_port 的 bug

6. 修复 multiple_uf_mock_robot
   robots 从 list 改为 dict[str, UFMockRobot],移除 keys 列表
   send_action 改用 dict items 遍历
   新增 MultipleUFMockRobot.cameras 聚合属性
This commit is contained in:
Vinman 2026-06-30 15:07:39 +08:00
parent 83a1c46441
commit bdc8ef05ab
72 changed files with 371 additions and 262 deletions

4
.gitignore vendored
View File

@ -6,8 +6,8 @@ __pycache__/
*$py.class *$py.class
*.so *.so
*.so.* *.so.*
!src/ufactory_lerobot/devices/umi/xvlib/lib*.so !src/lerobot_robot_ufactory/devices/umi/xvlib/lib*.so
!src/ufactory_lerobot/devices/umi/xvlib/lib*.so.* !src/lerobot_robot_ufactory/devices/umi/xvlib/lib*.so.*
*.egg *.egg
*.egg-info/ *.egg-info/
dist/ dist/

View File

@ -1,5 +1,5 @@
RobotConfig: robot:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig type: uf::robot
id: "uf_robot" id: "uf_robot"
robot_dof: 5 robot_dof: 5
control_space: "joint" control_space: "joint"
@ -7,20 +7,9 @@ RobotConfig:
gripper_type: 1 gripper_type: 1
start_joints: [0, 0, -90, 90, 0] start_joints: [0, 0, -90, 90, 0]
DatasetRecordConfig:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm5_gello_datas"
repo_id: "ufactory/xarm5_gello_datas"
single_task: "Pick up the purple grape and drop into the box on the left."
fps: 30
episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes
push_to_hub: False
# make sure to edit with your correct configurations! # make sure to edit with your correct configurations!
TeleoperatorConfig: teleop:
_target_: ufactory_lerobot.teleoperators.gello_teleop.GelloTeleopConfig type: uf::gello_teleop
port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0" port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0"
joint_ids: [1, 2, 4, 6, 7] joint_ids: [1, 2, 4, 6, 7]
joint_signs: [1, 1, -1, 1, 1] joint_signs: [1, 1, -1, 1, 1]
@ -28,3 +17,12 @@ TeleoperatorConfig:
gripper_id: 8 gripper_id: 8
torque_joint_ids: [3, 5] torque_joint_ids: [3, 5]
dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm5_gello_datas"
repo_id: "ufactory/xarm5_gello_datas"
single_task: "Pick up the purple grape and drop into the box on the left."
fps: 30
episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # time for resetting env between episodes
push_to_hub: False

View File

@ -1,5 +1,5 @@
RobotConfig: robot:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig type: uf::robot
id: "uf_robot" id: "uf_robot"
robot_dof: 6 robot_dof: 6
control_space: "joint" control_space: "joint"
@ -7,20 +7,9 @@ RobotConfig:
gripper_type: 1 gripper_type: 1
start_joints: [0, 0, -90, 0, 90, 0] start_joints: [0, 0, -90, 0, 90, 0]
DatasetRecordConfig:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm6_gello_datas"
repo_id: "ufactory/xarm6_gello_datas"
single_task: "Pick up the purple grape and drop into the box on the left."
fps: 30
episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes
push_to_hub: False
# make sure to edit with your correct configurations! # make sure to edit with your correct configurations!
TeleoperatorConfig: teleop:
_target_: ufactory_lerobot.teleoperators.gello_teleop.GelloTeleopConfig type: uf::gello_teleop
port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0" port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0"
joint_ids: [1, 2, 4, 5, 6, 7] joint_ids: [1, 2, 4, 5, 6, 7]
joint_signs: [1, 1, -1, 1, 1, 1] joint_signs: [1, 1, -1, 1, 1, 1]
@ -28,3 +17,12 @@ TeleoperatorConfig:
gripper_id: 8 gripper_id: 8
torque_joint_ids: [3] torque_joint_ids: [3]
dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm6_gello_datas"
repo_id: "ufactory/xarm6_gello_datas"
single_task: "Pick up the purple grape and drop into the box on the left."
fps: 30
episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # time for resetting env between episodes
push_to_hub: False

View File

@ -1,5 +1,5 @@
RobotConfig: robot:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig type: uf::robot
id: "uf_robot" id: "uf_robot"
robot_dof: 7 robot_dof: 7
control_space: "joint" control_space: "joint"
@ -7,21 +7,19 @@ RobotConfig:
gripper_type: 1 gripper_type: 1
start_joints: [0, 0, 0, 90, 0, 90, 0] start_joints: [0, 0, 0, 90, 0, 90, 0]
DatasetRecordConfig: # make sure to edit with your correct configurations!
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig teleop:
type: uf::gello_teleop
id: "gello_teleop"
port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0"
start_joints: [0, 0, 0, 90, 0, 90, 0]
dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default) # root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm6_gello_datas" root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm6_gello_datas"
repo_id: "ufactory/xarm6_gello_datas" repo_id: "ufactory/xarm6_gello_datas"
single_task: "Pick up the purple grape and drop into the box on the left." single_task: "Pick up the purple grape and drop into the box on the left."
fps: 30 fps: 30
episode_time_s: 60 # max duration for one episode episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes reset_time_s: 20 # time for resetting env between episodes
push_to_hub: False push_to_hub: False
# make sure to edit with your correct configurations!
TeleoperatorConfig:
_target_: ufactory_lerobot.teleoperators.gello_teleop.GelloTeleopConfig
id: "gello_teleop"
port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0"
start_joints: [0, 0, 0, 90, 0, 90, 0]

View File

@ -1,21 +1,29 @@
RobotConfig: robot:
_target_: ufactory_lerobot.robots.uf_mock_robot.UFMockRobotConfig type: uf::mock_robot
id: "uf_mock_robot" id: "uf_mock_robot"
control_space: "cartesian" control_space: "cartesian"
gripper_type: 2 gripper_type: 2
state_offset_action: 3 state_offset_action: 3
teleop_id: "pika_xxxx"
cameras: cameras:
fisheye: fisheye:
_target_: lerobot.cameras.opencv.configuration_opencv.OpenCVCameraConfig type: opencv
index_or_path: "/dev/v4l/by-id/usb-DECXIN_CAMERA_DECXIN_CAMERA_01.00.00-video-index0" index_or_path: "/dev/v4l/by-id/usb-DECXIN_CAMERA_DECXIN_CAMERA_01.00.00-video-index0"
width: 640 width: 640
height: 480 height: 480
fps: 30 fps: 30
fourcc: "MJPG" fourcc: "MJPG"
DatasetRecordConfig: # make sure to edit with your correct configurations!
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig teleop:
type: uf::pika_teleop
id: "pika_xxxx"
# port: "/dev/ttyUSB0"
scale_xyz: 1.5
tracker_to_robot_eef: [0, 0, 0, 180, -90, 0]
robot_base_pose: [400, 0, 400, 180, 0, 0]
dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default) # root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/pika_datas" root: "/home/uf/Data/lerobot_datas/record/ufactory/pika_datas"
repo_id: "ufactory/pika_datas" repo_id: "ufactory/pika_datas"
@ -24,11 +32,3 @@ DatasetRecordConfig:
episode_time_s: 60 # max duration for one episode episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes reset_time_s: 20 # (invalid) time for resetting env between episodes
push_to_hub: False push_to_hub: False
# make sure to edit with your correct configurations!
TeleoperatorConfig:
_target_: ufactory_lerobot.teleoperators.pika_teleop.PikaTeleopConfig
# port: "/dev/ttyUSB0"
scale_xyz: 1.5
tracker_to_robot_eef: [0, 0, 0, 180, -90, 0]
robot_base_pose: [400, 0, 400, 180, 0, 0]

View File

@ -1,5 +1,5 @@
RobotConfig: robot:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig type: uf::robot
id: "uf_robot" id: "uf_robot"
robot_dof: 7 robot_dof: 7
control_space: "cartesian" control_space: "cartesian"
@ -8,18 +8,24 @@ RobotConfig:
max_linear_velocity: 200 max_linear_velocity: 200
start_joints: [-0.2, -13.9, -0.3, 52.6, 0.9, 66.6, 0.2] start_joints: [-0.2, -13.9, -0.3, 52.6, 0.9, 66.6, 0.2]
start_tcp_pose: [400, 0, 400, 180, 0, 0] start_tcp_pose: [400, 0, 400, 180, 0, 0]
cameras: cameras:
fisheye: fisheye:
_target_: lerobot.cameras.opencv.configuration_opencv.OpenCVCameraConfig type: opencv
index_or_path: "/dev/v4l/by-id/usb-DECXIN_CAMERA_DECXIN_CAMERA_01.00.00-video-index0" index_or_path: "/dev/v4l/by-id/usb-DECXIN_CAMERA_DECXIN_CAMERA_01.00.00-video-index0"
width: 640 width: 640
height: 480 height: 480
fps: 30 fps: 30
fourcc: "MJPG" fourcc: "MJPG"
DatasetRecordConfig: # make sure to edit with your correct configurations!
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig teleop:
type: uf::pika_teleop
# port: "/dev/ttyUSB0"
scale_xyz: 1.5
tracker_to_robot_eef: [0, 0, 0, 180, -90, 0]
robot_base_pose: [400, 0, 400, 180, 0, 0]
dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default) # root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm7_pika_datas" root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm7_pika_datas"
repo_id: "ufactory/xarm7_pika_datas" repo_id: "ufactory/xarm7_pika_datas"
@ -28,11 +34,3 @@ DatasetRecordConfig:
episode_time_s: 60 # max duration for one episode episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes reset_time_s: 20 # (invalid) time for resetting env between episodes
push_to_hub: False push_to_hub: False
# make sure to edit with your correct configurations!
TeleoperatorConfig:
_target_: ufactory_lerobot.teleoperators.pika_teleop.PikaTeleopConfig
# port: "/dev/ttyUSB0"
scale_xyz: 1.5
tracker_to_robot_eef: [0, 0, 0, 180, -90, 0]
robot_base_pose: [400, 0, 400, 180, 0, 0]

View File

@ -1,5 +1,5 @@
RobotConfig: robot:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig type: uf::robot
id: "uf_robot" id: "uf_robot"
robot_dof: 7 robot_dof: 7
control_space: "cartesian" control_space: "cartesian"
@ -8,20 +8,25 @@ RobotConfig:
cameras: cameras:
overhead: overhead:
_target_: lerobot.cameras.realsense.configuration_realsense.RealSenseCameraConfig type: intelrealsense
serial_number_or_name: "Intel RealSense D435I" serial_number_or_name: "Intel RealSense D435I"
width: 640 width: 640
height: 480 height: 480
fps: 30 fps: 30
tool: tool:
_target_: lerobot.cameras.realsense.configuration_realsense.RealSenseCameraConfig type: intelrealsense
serial_number_or_name: "Intel RealSense D435" serial_number_or_name: "Intel RealSense D435"
width: 640 width: 640
height: 480 height: 480
fps: 30 fps: 30
DatasetRecordConfig: # space mouse
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig teleop:
type: uf::spacemouse_teleop
max_value: 300
use_gripper: False
dataset:
# root: /home/<user_name>/.cache/huggingface/lerobot (default) # root: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm7_pushT" root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm7_pushT"
repo_id: "ufactory/xarm7_pushT" repo_id: "ufactory/xarm7_pushT"
@ -30,9 +35,3 @@ DatasetRecordConfig:
episode_time_s: 60 # max duration for one episode episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes reset_time_s: 20 # (invalid) time for resetting env between episodes
push_to_hub: False push_to_hub: False
# space mouse
TeleoperatorConfig:
_target_: ufactory_lerobot.teleoperators.space_mouse.SpaceMouseTeleopConfig
max_value: 300
use_gripper: False

View File

@ -1,42 +1,44 @@
RobotConfig: robot:
_target_: ufactory_lerobot.robots.uf_mock_robot.MultipleUFMockRobotConfig type: uf::multiple_mock_robot
id: "multiple_uf_mock_robots" id: "multiple_uf_mock_robots"
robots: robots:
left: left:
_target_: ufactory_lerobot.robots.uf_mock_robot.UFMockRobotConfig type: uf::mock_robot
id: "left_uf_mock_robot" id: "left_uf_mock_robot"
control_space: "cartesian" control_space: "cartesian"
gripper_type: 1 gripper_type: 1
state_offset_action: 3 state_offset_action: 3
teleop_id: "umi_250801DR48FP26001318"
cameras: cameras:
fisheye: fisheye:
_target_: ufactory_lerobot.cameras.umi_camera.configuration_umi.UmiCameraConfig type: uf::umi_camera
serial_number: "250801DR48FP26001318" serial_number: "250801DR48FP26001318"
width: 1280 width: 1280
height: 1280 height: 1280
fps: 30 fps: 30
right: right:
_target_: ufactory_lerobot.robots.uf_mock_robot.UFMockRobotConfig type: uf::mock_robot
id: "right_uf_mock_robot" id: "right_uf_mock_robot"
control_space: "cartesian" control_space: "cartesian"
gripper_type: 1 gripper_type: 1
state_offset_action: 3 state_offset_action: 3
teleop_id: "umi_250801DR48FP26001295"
cameras: cameras:
fisheye: fisheye:
_target_: ufactory_lerobot.cameras.umi_camera.configuration_umi.UmiCameraConfig type: uf::umi_camera
serial_number: "250801DR48FP26001295" serial_number: "250801DR48FP26001295"
width: 1280 width: 1280
height: 1280 height: 1280
fps: 30 fps: 30
# make sure to edit with your correct configurations! # make sure to edit with your correct configurations!
TeleoperatorConfig: teleop:
_target_: ufactory_lerobot.teleoperators.umi_teleop.MultipleUmiTeleopConfig type: uf::multiple_umi_teleop
id: "multiple_teleops" id: "multiple_teleops"
teleops: teleops:
left: left:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig type: uf::umi_teleop
id: "left_teleop" id: "umi_250801DR48FP26001318"
serial_number: "250801DR48FP26001318" serial_number: "250801DR48FP26001318"
use_vive_tracker: False use_vive_tracker: False
vive_tracker_id: "LHR-555DC7BF" vive_tracker_id: "LHR-555DC7BF"
@ -44,8 +46,8 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90] tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 150, -90, 0, 0] robot_base_pose: [400, 0, 150, -90, 0, 0]
right: right:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig type: uf::umi_teleop
id: "right_teleop" id: "umi_250801DR48FP26001295"
serial_number: "250801DR48FP26001295" serial_number: "250801DR48FP26001295"
use_vive_tracker: False use_vive_tracker: False
vive_tracker_id: "LHR-2425BAD3" vive_tracker_id: "LHR-2425BAD3"
@ -53,15 +55,14 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90] tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 150, 90, 0, 0] robot_base_pose: [400, 0, 150, 90, 0, 0]
DatasetRecordConfig: dataset:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default) # root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/dual_xarm6_umi_datas" root: "/home/uf/Data/lerobot_datas/record/ufactory/dual_xarm6_umi_datas"
repo_id: "ufactory/dual_xarm6_umi_datas" repo_id: "ufactory/dual_umi_datas"
single_task: "Pick up the purple grape and drop into the box on the left." single_task: "Pick up the purple grape and drop into the box on the left."
fps: 60 fps: 60
episode_time_s: 60 # max duration for one episode episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes reset_time_s: 20 # time for resetting env between episodes
num_episodes: 100 num_episodes: 100
push_to_hub: False push_to_hub: False

View File

@ -1,12 +1,12 @@
RobotConfig: robot:
_target_: ufactory_lerobot.robots.uf_robot.MultipleUFRobotConfig type: uf::multiple_robot
id: "multiple_uf_robots" id: "multiple_uf_robots"
async_connect: True async_connect: True
async_configure: True async_configure: True
async_action: False async_action: False
robots: robots:
left: left:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig type: uf::robot
id: "left_uf_robot" id: "left_uf_robot"
robot_dof: 6 robot_dof: 6
control_space: "cartesian" control_space: "cartesian"
@ -15,16 +15,23 @@ RobotConfig:
max_linear_velocity: 200 max_linear_velocity: 200
start_joints: [-14.7, 25.6, -49.6, -282.5, 95.9, -65.9] start_joints: [-14.7, 25.6, -49.6, -282.5, 95.9, -65.9]
start_tcp_pose: [400, 0, 150, -90, 0, 0] start_tcp_pose: [400, 0, 150, -90, 0, 0]
teleop_id: "umi_250801DR48FP26001318"
# no_action: True
cameras: cameras:
fisheye: fisheye:
_target_: lerobot.cameras.opencv.configuration_opencv.OpenCVCameraConfig # type: uf::umi_camera
# serial_number: "250801DR48FB26001379"
# width: 1280
# height: 1280
# fps: 30
type: opencv
index_or_path: "/dev/v4l/by-id/usb-XVisio_Technology_XVisio_vSLAM_250801DR48FB26001379-video-index0" index_or_path: "/dev/v4l/by-id/usb-XVisio_Technology_XVisio_vSLAM_250801DR48FB26001379-video-index0"
width: 1280 width: 1280
height: 1280 height: 1280
fps: 100 fps: 100
fourcc: "YUYV" fourcc: "YUYV"
right: right:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig type: uf::robot
id: "right_uf_robot" id: "right_uf_robot"
robot_dof: 6 robot_dof: 6
control_space: "cartesian" control_space: "cartesian"
@ -33,9 +40,16 @@ RobotConfig:
max_linear_velocity: 200 max_linear_velocity: 200
start_joints: [14.5, 25.5, -49.6, -76.8, 95.9, 66.6] start_joints: [14.5, 25.5, -49.6, -76.8, 95.9, 66.6]
start_tcp_pose: [400, 0, 150, 90, 0, 0] start_tcp_pose: [400, 0, 150, 90, 0, 0]
teleop_id: "umi_250801DR48FP26001295"
# no_action: True
cameras: cameras:
fisheye: fisheye:
_target_: lerobot.cameras.opencv.configuration_opencv.OpenCVCameraConfig # type: uf::umi_camera
# serial_number: "250801DR48FB26001396"
# width: 1280
# height: 1280
# fps: 30
type: opencv
index_or_path: "/dev/v4l/by-id/usb-XVisio_Technology_XVisio_vSLAM_250801DR48FB26001396-video-index0" index_or_path: "/dev/v4l/by-id/usb-XVisio_Technology_XVisio_vSLAM_250801DR48FB26001396-video-index0"
width: 1280 width: 1280
height: 1280 height: 1280
@ -43,13 +57,13 @@ RobotConfig:
fourcc: "YUYV" fourcc: "YUYV"
# make sure to edit with your correct configurations! # make sure to edit with your correct configurations!
TeleoperatorConfig: teleop:
_target_: ufactory_lerobot.teleoperators.umi_teleop.MultipleUmiTeleopConfig type: uf::multiple_umi_teleop
id: "multiple_teleops" id: "multiple_teleops"
teleops: teleops:
left: left:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig type: uf::umi_teleop
id: "left_teleop" id: "umi_250801DR48FP26001318"
serial_number: "250801DR48FP26001318" serial_number: "250801DR48FP26001318"
use_vive_tracker: False use_vive_tracker: False
vive_tracker_id: "LHR-555DC7BF" vive_tracker_id: "LHR-555DC7BF"
@ -57,8 +71,8 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90] tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 150, -90, 0, 0] robot_base_pose: [400, 0, 150, -90, 0, 0]
right: right:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig type: uf::umi_teleop
id: "right_teleop" id: "umi_250801DR48FP26001295"
serial_number: "250801DR48FP26001295" serial_number: "250801DR48FP26001295"
use_vive_tracker: False use_vive_tracker: False
vive_tracker_id: "LHR-2425BAD3" vive_tracker_id: "LHR-2425BAD3"
@ -66,15 +80,14 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90] tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 150, 90, 0, 0] robot_base_pose: [400, 0, 150, 90, 0, 0]
DatasetRecordConfig: dataset:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default) # root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/dual_xarm6_umi_datas" root: "/home/uf/Data/lerobot_datas/record/ufactory/dual_xarm6_umi_datas"
repo_id: "ufactory/dual_xarm6_umi_datas" repo_id: "ufactory/dual_xarm6_umi_datas"
single_task: "Pick up the purple grape and drop into the box on the left." single_task: "Pick up the purple grape and drop into the box on the left."
fps: 60 fps: 60
episode_time_s: 60 # max duration for one episode episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes reset_time_s: 20 # time for resetting env between episodes
num_episodes: 100 num_episodes: 100
push_to_hub: False push_to_hub: False

View File

@ -1,19 +1,21 @@
RobotConfig: robot:
_target_: ufactory_lerobot.robots.uf_mock_robot.UFMockRobotConfig type: uf::mock_robot
id: "uf_mock_robot" id: "uf_mock_robot"
control_space: "cartesian" control_space: "cartesian"
gripper_type: 1 gripper_type: 1
state_offset_action: 3 state_offset_action: 3
teleop_id: "umi_250801DR48FP26001295"
cameras: cameras:
fisheye: fisheye:
_target_: ufactory_lerobot.cameras.umi_camera.configuration_umi.UmiCameraConfig type: uf::umi_camera
serial_number: "250801DR48FP26001295" serial_number: "250801DR48FP26001295"
width: 1280 width: 1280
height: 1280 height: 1280
fps: 100 fps: 100
TeleoperatorConfig: teleop:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig type: uf::umi_teleop
id: "umi_250801DR48FP26001295"
serial_number: "250801DR48FP26001295" serial_number: "250801DR48FP26001295"
use_vive_tracker: False use_vive_tracker: False
vive_tracker_id: "LHR-2425BAD3" vive_tracker_id: "LHR-2425BAD3"
@ -21,14 +23,13 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90] tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 400, 180, 0, 0] robot_base_pose: [400, 0, 400, 180, 0, 0]
DatasetRecordConfig: dataset:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default) # root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/umi_datas" root: "/home/uf/Data/lerobot_datas/record/ufactory/umi_datas"
repo_id: "ufactory/umi_datas" repo_id: "ufactory/umi_datas"
single_task: "Pick up the purple grape and drop into the box on the left." single_task: "Pick up the purple grape and drop into the box on the left."
fps: 60 fps: 60
episode_time_s: 60 # max duration for one episode episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes reset_time_s: 20 # time for resetting env between episodes
num_episodes: 100 num_episodes: 100
push_to_hub: False push_to_hub: False

View File

@ -1,5 +1,5 @@
RobotConfig: robot:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig type: uf::robot
id: "uf_robot" id: "uf_robot"
robot_dof: 6 robot_dof: 6
control_space: "cartesian" control_space: "cartesian"
@ -10,15 +10,15 @@ RobotConfig:
start_tcp_pose: [400, 0, 400, 180, 0, 0] start_tcp_pose: [400, 0, 400, 180, 0, 0]
cameras: cameras:
fisheye: fisheye:
_target_: lerobot.cameras.opencv.configuration_opencv.OpenCVCameraConfig type: opencv
index_or_path: "/dev/v4l/by-id/usb-XVisio_Technology_XVisio_vSLAM_250801DR48FB26001396-video-index0" index_or_path: "/dev/v4l/by-id/usb-XVisio_Technology_XVisio_vSLAM_250801DR48FB26001396-video-index0"
width: 1280 width: 1280
height: 1280 height: 1280
fps: 100 fps: 100
fourcc: "YUYV" fourcc: "YUYV"
TeleoperatorConfig: teleop:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig type: uf::umi_teleop
serial_number: "250801DR48FP26001295" serial_number: "250801DR48FP26001295"
use_vive_tracker: False use_vive_tracker: False
vive_tracker_id: "LHR-2425BAD3" vive_tracker_id: "LHR-2425BAD3"
@ -26,14 +26,13 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90] tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 400, 180, 0, 0] robot_base_pose: [400, 0, 400, 180, 0, 0]
DatasetRecordConfig: dataset:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default) # root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm6_umi_datas" root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm6_umi_datas"
repo_id: "ufactory/xarm6_umi_datas" repo_id: "ufactory/xarm6_umi_datas"
single_task: "Pick up the purple grape and drop into the box on the left." single_task: "Pick up the purple grape and drop into the box on the left."
fps: 60 fps: 60
episode_time_s: 60 # max duration for one episode episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes reset_time_s: 20 # time for resetting env between episodes
num_episodes: 100 num_episodes: 100
push_to_hub: False push_to_hub: False

View File

@ -3,14 +3,14 @@ requires = ["setuptools>=68.0", "wheel>=0.42"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "ufactory_lerobot" name = "lerobot_robot_ufactory"
version = "0.1.0" version = "0.1.0"
description = "UFACTORY 机械臂与 LeRobot 框架集成" description = "UFACTORY 机械臂与 LeRobot 框架集成"
readme = "README.md" readme = "README.md"
license = { text = "Apache-2.0" } license = { text = "Apache-2.0" }
requires-python = ">=3.10" requires-python = ">=3.10"
authors = [ authors = [
{ name = "ufactory_lerobot Contributors" }, { name = "lerobot_robot_ufactory Contributors" },
] ]
keywords = ["robotics", "lerobot", "ufactory", "xarm", "imitation-learning"] keywords = ["robotics", "lerobot", "ufactory", "xarm", "imitation-learning"]
classifiers = [ classifiers = [
@ -32,11 +32,11 @@ dependencies = [
] ]
[project.scripts] [project.scripts]
uf-robot-teleop = "ufactory_lerobot.scripts.uf_robot_teleop:main" uf-robot-teleop = "lerobot_robot_ufactory.scripts.uf_robot_teleop:main"
uf-lerobot-record = "ufactory_lerobot.scripts.uf_lerobot_record:main" uf-lerobot-record = "lerobot_robot_ufactory.scripts.uf_lerobot_record:main"
uf-lerobot-eval = "ufactory_lerobot.scripts.uf_lerobot_eval:main" uf-lerobot-eval = "lerobot_robot_ufactory.scripts.uf_lerobot_eval:main"
uf-vive-calibrate = "ufactory_lerobot.scripts.vive_calibrate:main" uf-vive-calibrate = "lerobot_robot_ufactory.scripts.vive_calibrate:main"
uf-camera-view = "ufactory_lerobot.scripts.uf_camera_view:main" uf-camera-view = "lerobot_robot_ufactory.scripts.uf_camera_view:main"
[project.optional-dependencies] [project.optional-dependencies]
# GELLO 遥操作 # GELLO 遥操作
@ -64,7 +64,7 @@ include-package-data = true
where = ["src"] where = ["src"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
"ufactory_lerobot.devices.umi.xvlib" = ["*.so", "*.so.*"] "lerobot_robot_ufactory.devices.umi.xvlib" = ["*.so", "*.so.*"]
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 100

View File

@ -1,6 +1,6 @@
from ufactory_lerobot.cameras.utils import make_cameras_from_configs as _uf_make_cameras_from_configs from lerobot_robot_ufactory.cameras.utils import make_cameras_from_configs as _uf_make_cameras_from_configs
from ufactory_lerobot.robots.utils import make_robot_from_config as _uf_make_robot_from_config from lerobot_robot_ufactory.robots.utils import make_robot_from_config as _uf_make_robot_from_config
from ufactory_lerobot.teleoperators.utils import make_teleoperator_from_config as _uf_make_teleoperator_from_config from lerobot_robot_ufactory.teleoperators.utils import make_teleoperator_from_config as _uf_make_teleoperator_from_config
import lerobot.cameras as _lerobot_cameras import lerobot.cameras as _lerobot_cameras
import lerobot.robots as _lerobot_robot import lerobot.robots as _lerobot_robot
import lerobot.teleoperators as _lerobot_teleoperators import lerobot.teleoperators as _lerobot_teleoperators
@ -15,11 +15,11 @@ _lerobot_cameras_utils.make_cameras_from_configs = _uf_make_cameras_from_configs
_lerobot_robot_utils.make_robot_from_config = _uf_make_robot_from_config _lerobot_robot_utils.make_robot_from_config = _uf_make_robot_from_config
_lerobot_teleoperators_utils.make_teleoperator_from_config = _uf_make_teleoperator_from_config _lerobot_teleoperators_utils.make_teleoperator_from_config = _uf_make_teleoperator_from_config
# register plugin # register plugin
import ufactory_lerobot.cameras.umi_camera import lerobot_robot_ufactory.cameras.umi_camera
import ufactory_lerobot.robots.uf_robot import lerobot_robot_ufactory.robots.uf_robot
import ufactory_lerobot.robots.uf_mock_robot import lerobot_robot_ufactory.robots.uf_mock_robot
import ufactory_lerobot.teleoperators.uf_mock_teleop import lerobot_robot_ufactory.teleoperators.uf_mock_teleop
import ufactory_lerobot.teleoperators.gello_teleop import lerobot_robot_ufactory.teleoperators.gello_teleop
import ufactory_lerobot.teleoperators.pika_teleop import lerobot_robot_ufactory.teleoperators.pika_teleop
import ufactory_lerobot.teleoperators.space_mouse import lerobot_robot_ufactory.teleoperators.space_mouse
import ufactory_lerobot.teleoperators.umi_teleop import lerobot_robot_ufactory.teleoperators.umi_teleop

View File

@ -51,7 +51,7 @@ class UmiCamera(Camera):
self.rotation: int | None = get_cv2_rotation(config.rotation) self.rotation: int | None = get_cv2_rotation(config.rotation)
self.last_frame = None self.last_frame = None
from ufactory_lerobot.devices.umi.xvlib import XVLib from lerobot_robot_ufactory.devices.umi.xvlib import XVLib
self.xvlib = XVLib(self.serial_number) self.xvlib = XVLib(self.serial_number)
self.xvlib.xv_color_camera_init() self.xvlib.xv_color_camera_init()

View File

@ -0,0 +1,28 @@
"""上下文变量,用于跨模块共享运行时对象,避免属性注入。"""
import contextvars
_active_teleops: contextvars.ContextVar = contextvars.ContextVar("active_teleops", default={})
def register_teleop(teleop) -> None:
"""注册 teleop, 以 teleop.id 为 key"""
teleops = _active_teleops.get()
teleops[teleop.id] = teleop
_active_teleops.set(teleops)
def unregister_teleop(teleop) -> None:
"""移除 teleop"""
teleops = _active_teleops.get()
teleops.pop(teleop.id, None)
_active_teleops.set(teleops)
def get_active_teleop(teleop_id: str | None = None):
"""获取 active teleop。不传 id 返回第一个;传 id 返回对应 teleop。"""
teleops = _active_teleops.get()
if not teleops:
return None
if teleop_id is not None:
return teleops.get(teleop_id)
return next(iter(teleops.values()))

View File

@ -72,6 +72,7 @@ class PikaDevice(object):
# _pika_sense_port = None # _pika_sense_port = None
# _pika_gripper_port = None # _pika_gripper_port = None
# _lock = threading.Lock() # _lock = threading.Lock()
PIKA_DEVICE_MAP = {}
def __init__(self, dev_type=1, **kwargs): def __init__(self, dev_type=1, **kwargs):
""" """
@ -144,6 +145,9 @@ class PikaDevice(object):
if self._dev_type not in [1, 3]: if self._dev_type not in [1, 3]:
return None return None
if self._pika_sense is None: if self._pika_sense is None:
if self._pika_sense_port in self.PIKA_DEVICE_MAP:
self._pika_sense = self.PIKA_DEVICE_MAP[self._pika_sense_port]
return self._pika_sense
from pika.sense import Sense from pika.sense import Sense
# 初始化Sense对象 # 初始化Sense对象
self._pika_sense = Sense(port=self._pika_sense_port) self._pika_sense = Sense(port=self._pika_sense_port)
@ -152,6 +156,7 @@ class PikaDevice(object):
logger.error('连接Pika Sense设备失败') logger.error('连接Pika Sense设备失败')
exit(1) exit(1)
logger.info('Pika Sense设备连接成功') logger.info('Pika Sense设备连接成功')
self.PIKA_DEVICE_MAP[self._pika_sense_port] = self._pika_sense # 注册共享
# 配置Vive Tracker可选 # 配置Vive Tracker可选
# sense.set_vive_tracker_config(config_path='path/to/config', lh_config='lighthouse_config') # sense.set_vive_tracker_config(config_path='path/to/config', lh_config='lighthouse_config')
@ -186,16 +191,19 @@ class PikaDevice(object):
if self._dev_type not in [2, 3]: if self._dev_type not in [2, 3]:
return None return None
if self._pika_gripper is None: if self._pika_gripper is None:
if self._dev_type in [2, 3]: if self._pika_gripper_port in self.PIKA_DEVICE_MAP:
from pika.gripper import Gripper self._pika_gripper = self.PIKA_DEVICE_MAP[self._pika_gripper_port]
self._pika_gripper = Gripper(port=self._pika_gripper_port) return self._pika_gripper
# 连接设备 from pika.gripper import Gripper
if not self._pika_gripper.connect(): self._pika_gripper = Gripper(port=self._pika_gripper_port)
logger.error('连接Pika Gripper设备失败') self.PIKA_DEVICE_MAP[self._pika_gripper_port] = self._pika_gripper # 注册共享
if self._dev_type in [1, 3]: # 连接设备
self.pika_sense.disconnect() if not self._pika_gripper.connect():
exit(1) logger.error('连接Pika Gripper设备失败')
logger.info('Pika Gripper设备连接成功') if self._dev_type in [1, 3]:
self.pika_sense.disconnect()
exit(1)
logger.info('Pika Gripper设备连接成功')
return self._pika_gripper return self._pika_gripper

View File

@ -15,60 +15,61 @@ class MultipleUFMockRobot(Robot):
def __init__(self, config: MultipleUFMockRobotConfig): def __init__(self, config: MultipleUFMockRobotConfig):
super().__init__(config) super().__init__(config)
self.config = config self.config = config
self.keys = [] self.robots: dict[str, UFMockRobot] = {}
self.robots = []
for key, robot_config in self.config.robots.items(): for key, robot_config in self.config.robots.items():
self.keys.append(key) self.robots[key] = UFMockRobot(robot_config, prefix=key)
self.robots.append(UFMockRobot(robot_config, prefix=key))
self.cameras = {}
for robot in self.robots.values():
self.cameras.update(robot.cameras)
@property @property
def observation_features(self) -> dict: def observation_features(self) -> dict:
observation_features = {} observation_features = {}
for robot in self.robots: for robot in self.robots.values():
observation_features.update(robot.observation_features) observation_features.update(robot.observation_features)
return observation_features return observation_features
@property @property
def action_features(self) -> dict: def action_features(self) -> dict:
action_features = {} action_features = {}
for robot in self.robots: for robot in self.robots.values():
action_features.update(robot.action_features) action_features.update(robot.action_features)
return action_features return action_features
@property @property
def is_connected(self) -> bool: def is_connected(self) -> bool:
return all(robot.is_connected for robot in self.robots) return all(robot.is_connected for robot in self.robots.values())
@property @property
def is_calibrated(self) -> bool: def is_calibrated(self) -> bool:
return all(robot.is_calibrated for robot in self.robots) return all(robot.is_calibrated for robot in self.robots.values())
def connect(self, calibrate: bool = True) -> None: def connect(self, calibrate: bool = True) -> None:
for robot in self.robots: for robot in self.robots.values():
robot.connect(calibrate=calibrate) robot.connect(calibrate=calibrate)
def calibrate(self) -> None: def calibrate(self) -> None:
for robot in self.robots: for robot in self.robots.values():
robot.calibrate() robot.calibrate()
def configure(self) -> None: def configure(self) -> None:
for robot in self.robots: for robot in self.robots.values():
robot.configure() robot.configure()
def disconnect(self) -> None: def disconnect(self) -> None:
for robot in self.robots: for robot in self.robots.values():
robot.disconnect() robot.disconnect()
def get_observation(self) -> RobotObservation: def get_observation(self) -> RobotObservation:
observations = [robot.get_observation() for robot in self.robots] observations = [robot.get_observation() for robot in self.robots.values()]
combined_observation = RobotObservation() combined_observation = RobotObservation()
for obs in observations: for obs in observations:
combined_observation.update(obs) combined_observation.update(obs)
return combined_observation return combined_observation
def send_action(self, action: RobotAction) -> RobotAction: def send_action(self, action: RobotAction) -> RobotAction:
for i in range(len(self.keys)): for key, robot in self.robots.items():
key = self.keys[i]
action_subset = {k: v for k, v in action.items() if k.startswith(f"{key}.")} action_subset = {k: v for k, v in action.items() if k.startswith(f"{key}.")}
self.robots[i].send_action(action_subset) robot.send_action(action_subset)
return action return action

View File

@ -1,11 +1,13 @@
from dataclasses import dataclass from dataclasses import dataclass, field
from lerobot.robots import RobotConfig from lerobot.robots import RobotConfig
from .uf_mock_robot_config import UFMockRobotConfig from .uf_mock_robot_config import UFMockRobotConfig
@RobotConfig.register_subclass("uf::multiple_mock_robot") @RobotConfig.register_subclass("uf::multiple_mock_robot")
@dataclass @dataclass
class MultipleUFMockRobotConfig(RobotConfig): class MultipleUFMockRobotConfig(RobotConfig):
robots: dict[str, UFMockRobotConfig] robots: dict[str, RobotConfig] = field(
default_factory=lambda: {}
)
def __post_init__(self): def __post_init__(self):
super().__post_init__() super().__post_init__()

View File

@ -1,6 +1,7 @@
import numpy as np import numpy as np
from lerobot.cameras.utils import make_cameras_from_configs from lerobot.cameras.utils import make_cameras_from_configs
from lerobot.robots import Robot from lerobot.robots import Robot
from lerobot_robot_ufactory.context import get_active_teleop
from .uf_mock_robot_config import UFMockRobotConfig from .uf_mock_robot_config import UFMockRobotConfig
@ -31,7 +32,6 @@ class UFMockRobot(Robot):
self._jnt_obs_has_vel = config.observe_joint_vel if self._control_space == "joint" else False self._jnt_obs_has_vel = config.observe_joint_vel if self._control_space == "joint" else False
self._is_connected = False self._is_connected = False
self._is_calibrated =True self._is_calibrated =True
self._teleop = self.config.teleop
self._cache_num = self.config.state_offset_action self._cache_num = self.config.state_offset_action
self._teleop_actions = [] self._teleop_actions = []
@ -98,7 +98,8 @@ class UFMockRobot(Robot):
pass # CHECK! currently No-op pass # CHECK! currently No-op
def get_observation(self) -> dict[str, np.ndarray]: def get_observation(self) -> dict[str, np.ndarray]:
new_act = self._teleop.get_action() teleop = get_active_teleop(self.config.teleop_id)
new_act = teleop.get_action() if teleop else {}
# Capture images from cameras # Capture images from cameras
for cam_key, cam in self.cameras.items(): for cam_key, cam in self.cameras.items():
new_act[f"{self.prefix}{cam_key}"] = cam.async_read() new_act[f"{self.prefix}{cam_key}"] = cam.async_read()

View File

@ -5,6 +5,7 @@ from lerobot.robots import RobotConfig
@RobotConfig.register_subclass("uf::mock_robot") @RobotConfig.register_subclass("uf::mock_robot")
@dataclass @dataclass
class UFMockRobotConfig(RobotConfig): class UFMockRobotConfig(RobotConfig):
teleop_id: str
cameras: dict[str, CameraConfig] = field( cameras: dict[str, CameraConfig] = field(
default_factory=lambda: {} default_factory=lambda: {}
) )
@ -13,7 +14,6 @@ class UFMockRobotConfig(RobotConfig):
control_space: str = "joint" control_space: str = "joint"
gripper_type: int = 1 # 1: xArm Gripper, 10: Pika Gripper gripper_type: int = 1 # 1: xArm Gripper, 10: Pika Gripper
observe_joint_vel: bool = False # only effective in joint control mode observe_joint_vel: bool = False # only effective in joint control mode
teleop: None = None # from lerobot.teleoperators import Teleoperator
state_offset_action: int = 3 # the number of previous teleop actions to be included in the observation state_offset_action: int = 3 # the number of previous teleop actions to be included in the observation
def __post_init__(self): def __post_init__(self):

View File

@ -1,11 +1,13 @@
from dataclasses import dataclass from dataclasses import dataclass, field
from lerobot.robots import RobotConfig from lerobot.robots import RobotConfig
from .uf_robot_config import UFRobotConfig from .uf_robot_config import UFRobotConfig
@RobotConfig.register_subclass("uf::multiple_robot") @RobotConfig.register_subclass("uf::multiple_robot")
@dataclass @dataclass
class MultipleUFRobotConfig(RobotConfig): class MultipleUFRobotConfig(RobotConfig):
robots: dict[str, UFRobotConfig] robots: dict[str, RobotConfig] = field(
default_factory=lambda: {}
)
async_connect: bool = True async_connect: bool = True
async_configure: bool = True async_configure: bool = True
async_action: bool = False async_action: bool = False

View File

@ -10,7 +10,7 @@ from dataclasses import dataclass
from threading import Thread, Event, Lock from threading import Thread, Event, Lock
from lerobot.robots import Robot from lerobot.robots import Robot
from lerobot.cameras.utils import make_cameras_from_configs from lerobot.cameras.utils import make_cameras_from_configs
from ufactory_lerobot.devices.pika import PikaDevice from lerobot_robot_ufactory.devices.pika import PikaDevice
from .uf_robot_config import UFRobotConfig from .uf_robot_config import UFRobotConfig
from xarm.wrapper import XArmAPI from xarm.wrapper import XArmAPI
from xarm.core.utils import convert from xarm.core.utils import convert

View File

@ -0,0 +1,66 @@
#!/usr/bin/env python
"""Download XVSDK binaries and UMI shared libraries after install."""
import os
import sys
import urllib.request
import subprocess
import site
RESOURCES = "https://raw.githubusercontent.com/xArm-Developer/ufactory_resources/main/fastumi"
FILES = [
# (url_path, subdir)
("libxvlib.so", ""),
("opencv/libopencv_core.so.4.2", ""),
("opencv/libopencv_imgproc.so.4.2", ""),
]
DEB_FILE = "sdk/XVSDK_focal_amd64.deb"
def get_pkg_dir():
"""Find the installed ufactory_lerobot package directory."""
for sp in site.getsitepackages():
path = os.path.join(sp, "ufactory_lerobot", "devices", "umi", "xvlib")
if os.path.isdir(path):
return path
# fallback: try user site
usp = site.getusersitepackages()
if usp:
return os.path.join(usp, "ufactory_lerobot", "devices", "umi", "xvlib")
sys.exit("Error: cannot find installed ufactory_lerobot package.")
def download(url, dest):
print(f" -> {dest}")
os.makedirs(os.path.dirname(dest), exist_ok=True)
urllib.request.urlretrieve(url, dest)
def main():
pkg_dir = get_pkg_dir()
print(f"Installing to: {pkg_dir}\n")
for name, subdir in FILES:
url = f"{RESOURCES}/{name}"
dest = os.path.join(pkg_dir, os.path.basename(name))
print(f"Downloading {name} ...")
download(url, dest)
# .deb: download to /tmp and install
deb_url = f"{RESOURCES}/{DEB_FILE}"
deb_path = "/tmp/XVSDK_focal_amd64.deb"
print(f"\nDownloading {DEB_FILE} ...")
download(deb_url, deb_path)
print("\nInstalling XVSDK ...")
ret = subprocess.run(["sudo", "dpkg", "-i", deb_path]).returncode
if ret != 0:
subprocess.run(["sudo", "apt", "install", "-y", "--fix-broken"])
os.unlink(deb_path)
print("\nDone.")
if __name__ == "__main__":
main()

View File

@ -1,15 +1,13 @@
import yaml import sys
import argparse import argparse
import logging import logging
import time import time
import math import math
import torch
from pathlib import Path from pathlib import Path
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from pprint import pformat from pprint import pformat
from contextlib import nullcontext
import numpy as np import numpy as np
import ufactory_lerobot # patch import lerobot_robot_ufactory # patch
from lerobot.scripts.lerobot_record import register_third_party_plugins from lerobot.scripts.lerobot_record import register_third_party_plugins
from lerobot.datasets.pipeline_features import aggregate_pipeline_dataset_features, create_initial_features from lerobot.datasets.pipeline_features import aggregate_pipeline_dataset_features, create_initial_features
from lerobot.datasets.utils import build_dataset_frame, combine_feature_dicts from lerobot.datasets.utils import build_dataset_frame, combine_feature_dicts
@ -39,8 +37,8 @@ from lerobot.configs import parser
from lerobot.configs.policies import PreTrainedConfig from lerobot.configs.policies import PreTrainedConfig
from lerobot.scripts.lerobot_record import DatasetRecordConfig from lerobot.scripts.lerobot_record import DatasetRecordConfig
from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.lerobot_dataset import LeRobotDataset
from ufactory_lerobot.utils.utils import instantiate_from_dict, init_keyboard_listener from lerobot_robot_ufactory.utils.utils import init_keyboard_listener
from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations from lerobot_robot_ufactory.devices.umi.vive_tracker.transformations import Transformations
def continuous_rotvec(new_rv, prev_rv): def continuous_rotvec(new_rv, prev_rv):
@ -98,6 +96,7 @@ def compute_target_axis_angle(rot_prev, rot_delta):
class EvalConfig: class EvalConfig:
robot: RobotConfig robot: RobotConfig
dataset: DatasetRecordConfig dataset: DatasetRecordConfig
teleop: dict = None # no use
# Whether to control the robot with a policy # Whether to control the robot with a policy
policy: PreTrainedConfig | None = None policy: PreTrainedConfig | None = None
n_episodes: int = 50 n_episodes: int = 50
@ -414,26 +413,19 @@ def eval_loop(cfg: EvalConfig, relative=False, rx_continuous=False):
if not is_headless() and listener is not None: if not is_headless() and listener is not None:
listener.stop() listener.stop()
@parser.wrap()
def get_cfg(cfg: EvalConfig) -> EvalConfig:
return cfg
def main(): def main():
parser = argparse.ArgumentParser(description='configuration args') parser = argparse.ArgumentParser(description='configuration args')
parser.add_argument('-c', '--config', type=str, required=True,
help='configuration file path, e.g.my_config.yaml')
parser.add_argument('--policy.path', type=str, required=True,
help='configuration file path, e.g.my_config.yaml')
parser.add_argument('--relative', action='store_true', help='is relative motion or not') parser.add_argument('--relative', action='store_true', help='is relative motion or not')
parser.add_argument('--rx_continuous', action='store_true', help='rx continuous or not') parser.add_argument('--rx_continuous', action='store_true', help='rx continuous or not')
args = parser.parse_args() args, unknown = parser.parse_known_args()
try: sys.argv = [sys.argv[0]] + unknown
with open(args.config, 'r') as f: register_third_party_plugins()
cfg = yaml.safe_load(f) cfg = get_cfg()
except Exception as e: eval_loop(cfg, args.relative, args.rx_continuous)
print(f"Error loading config yaml file: {e}")
else:
register_third_party_plugins()
config = instantiate_from_dict(cfg)
eval_cfg = EvalConfig(robot=config["RobotConfig"], dataset=config["DatasetRecordConfig"])
eval_loop(eval_cfg, args.relative, args.rx_continuous)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,4 +1,4 @@
import yaml import sys
import copy import copy
import time import time
import queue import queue
@ -7,11 +7,11 @@ import logging
import shutil import shutil
import threading import threading
from pathlib import Path from pathlib import Path
import ufactory_lerobot # patch import lerobot_robot_ufactory # patch
from lerobot.scripts.lerobot_record import * from lerobot.scripts.lerobot_record import *
from ufactory_lerobot.teleoperators.uf_mock_teleop import UFMockTeleop from lerobot_robot_ufactory.teleoperators.uf_mock_teleop import UFMockTeleop
from ufactory_lerobot.teleoperators.base_teleop import UFBaseTeleop from lerobot_robot_ufactory.teleoperators.base_teleop import UFBaseTeleop
from ufactory_lerobot.utils.utils import instantiate_from_dict, init_keyboard_listener from lerobot_robot_ufactory.utils.utils import init_keyboard_listener
def _get_dataset_writer(dataset): def _get_dataset_writer(dataset):
@ -344,17 +344,14 @@ def record_loop(
timestamp = time.perf_counter() - start_episode_t timestamp = time.perf_counter() - start_episode_t
@parser.wrap()
def record(cfg: RecordConfig, async_save: bool = False) -> LeRobotDataset: def record(cfg: RecordConfig, async_save: bool = False) -> LeRobotDataset:
init_logging() init_logging()
logging.info(pformat(asdict(cfg))) logging.info(pformat(asdict(cfg)))
if cfg.display_data: if cfg.display_data:
init_rerun(session_name="recording") init_rerun(session_name="recording")
teleop = make_teleoperator_from_config(cfg.teleop) if cfg.teleop is not None else None
if hasattr(cfg.robot, "teleop"):
cfg.robot.teleop = teleop
robot = make_robot_from_config(cfg.robot) robot = make_robot_from_config(cfg.robot)
teleop = make_teleoperator_from_config(cfg.teleop) if cfg.teleop is not None else None
teleop_action_processor, robot_action_processor, robot_observation_processor = make_default_processors() teleop_action_processor, robot_action_processor, robot_observation_processor = make_default_processors()
@ -592,31 +589,28 @@ def record(cfg: RecordConfig, async_save: bool = False) -> LeRobotDataset:
log_say("Exiting", cfg.play_sounds) log_say("Exiting", cfg.play_sounds)
return dataset return dataset
@parser.wrap()
def get_cfg(cfg: RecordConfig) -> RecordConfig:
return cfg
def main(): def main():
parser = argparse.ArgumentParser(description='configuration args') parser = argparse.ArgumentParser(description='configuration args')
parser.add_argument('-c', '--config', type=str, required=True, parser.add_argument('-r',
help='configuration file path, e.g.my_config.yaml')
parser.add_argument('-r', '--resume',
action='store_true', # specify --resume if resume needs to be True action='store_true', # specify --resume if resume needs to be True
default=False, default=False,
help='Whether contitue recording on existing dataset (default: False)') help='Whether contitue recording on existing dataset (default: False)')
parser.add_argument('-a', '--async-save', parser.add_argument('-a', '--async_save',
action='store_true', action='store_true',
default=False, default=False,
help='Enable async background saving (default: False)') help='Enable async background saving (default: False)')
args = parser.parse_args() args, unknown = parser.parse_known_args()
try: sys.argv = [sys.argv[0]] + unknown
with open(Path(args.config).expanduser(), 'r') as f: register_third_party_plugins()
cfg = yaml.safe_load(f) cfg = get_cfg()
except Exception as e: if args.r:
print(f"Error loading config yaml file: {e}") cfg.resume = True
else: cfg.play_sounds = False
register_third_party_plugins() record(cfg, async_save=args.async_save)
config = instantiate_from_dict(cfg)
record_cfg = RecordConfig(resume=args.resume, play_sounds=False, robot=config["RobotConfig"], dataset=config["DatasetRecordConfig"], teleop=config["TeleoperatorConfig"])
record(record_cfg, async_save=args.async_save)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,11 +1,11 @@
import yaml import sys
import argparse import argparse
import logging import logging
import time import time
from pathlib import Path from pathlib import Path
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from pprint import pformat from pprint import pformat
import ufactory_lerobot # patch import lerobot_robot_ufactory # patch
from lerobot.scripts.lerobot_record import register_third_party_plugins from lerobot.scripts.lerobot_record import register_third_party_plugins
from lerobot.processor import ( from lerobot.processor import (
make_default_processors, make_default_processors,
@ -23,14 +23,16 @@ from lerobot.utils.robot_utils import precise_sleep
from lerobot.utils.utils import ( from lerobot.utils.utils import (
init_logging, init_logging,
) )
from ufactory_lerobot.utils.utils import instantiate_from_dict, is_headless, init_keyboard_listener from lerobot.configs import parser
from ufactory_lerobot.teleoperators.base_teleop import UFBaseTeleop from lerobot_robot_ufactory.utils.utils import is_headless, init_keyboard_listener
from lerobot_robot_ufactory.teleoperators.base_teleop import UFBaseTeleop
@dataclass @dataclass
class TeleopConfig: class TeleopConfig:
robot: RobotConfig robot: RobotConfig
teleop: TeleoperatorConfig teleop: TeleoperatorConfig
dataset: dict = None # no use
fps: int = 30 fps: int = 30
@ -167,24 +169,17 @@ def teleop_loop(cfg: TeleopConfig):
if is_evt and listener is not None: if is_evt and listener is not None:
listener.stop() listener.stop()
@parser.wrap()
def get_cfg(cfg: TeleopConfig) -> TeleopConfig:
return cfg
def main(): def main():
parser = argparse.ArgumentParser(description='configuration args') parser = argparse.ArgumentParser(description='configuration args')
parser.add_argument('-c', '--config', type=str, required=True, args, unknown = parser.parse_known_args()
help='configuration file path, e.g.my_config.yaml') sys.argv = [sys.argv[0]] + unknown
parser.add_argument('-f', '--fps', type=int, default=30, register_third_party_plugins()
help='control loop frequency in Hz (default: 30)') cfg = get_cfg()
args = parser.parse_args() teleop_loop(cfg)
try:
with open(Path(args.config).expanduser(), 'r') as f:
cfg = yaml.safe_load(f)
except Exception as e:
print(f"Error loading config yaml file: {e}")
else:
register_third_party_plugins()
config = instantiate_from_dict(cfg, ignore_cameras=True)
teleop_cfg = TeleopConfig(robot=config["RobotConfig"], teleop=config["TeleoperatorConfig"], fps=args.fps)
teleop_loop(teleop_cfg)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,4 +1,5 @@
from lerobot.teleoperators import Teleoperator from lerobot.teleoperators import Teleoperator
from lerobot_robot_ufactory.context import register_teleop, unregister_teleop
class UFBaseTeleop(Teleoperator): class UFBaseTeleop(Teleoperator):
@ -9,6 +10,12 @@ class UFBaseTeleop(Teleoperator):
super().__init__(config) super().__init__(config)
self.config = config self.config = config
def connect(self, calibrate: bool = False) -> None:
register_teleop(self)
def disconnect(self):
unregister_teleop(self)
def set_teleop_enabled(self, enabled: bool, obs=None): def set_teleop_enabled(self, enabled: bool, obs=None):
""" """
启用/停用遥操作 启用/停用遥操作

View File

@ -103,6 +103,7 @@ class GelloTeleop(UFBaseTeleop):
self.configure() self.configure()
self._is_connected = True self._is_connected = True
super().connect(calibrate)
logger.info(f"{self} connected.") logger.info(f"{self} connected.")
@property @property

View File

@ -5,8 +5,8 @@ import math
from typing import Any from typing import Any
from threading import Thread, Event, Lock from threading import Thread, Event, Lock
from lerobot.utils.errors import DeviceNotConnectedError from lerobot.utils.errors import DeviceNotConnectedError
from ufactory_lerobot.devices.pika import PikaDevice from lerobot_robot_ufactory.devices.pika import PikaDevice
from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations from lerobot_robot_ufactory.devices.umi.vive_tracker.transformations import Transformations
from ..base_teleop import UFBaseTeleop from ..base_teleop import UFBaseTeleop
from .pika_teleop_config import PikaTeleopConfig from .pika_teleop_config import PikaTeleopConfig
@ -87,6 +87,7 @@ class PikaTeleop(UFBaseTeleop, Thread):
pass pass
def connect(self, calibrate: bool = False) -> None: def connect(self, calibrate: bool = False) -> None:
super().connect(calibrate)
self.start() self.start()
def disconnect(self): def disconnect(self):

View File

@ -40,6 +40,7 @@ class MultipleUmiTeleop(UFBaseTeleop):
return all(teleop.is_calibrated for teleop in self.teleops.values()) return all(teleop.is_calibrated for teleop in self.teleops.values())
def connect(self, calibrate: bool = True) -> None: def connect(self, calibrate: bool = True) -> None:
super().connect(calibrate)
for teleop in self.teleops.values(): for teleop in self.teleops.values():
teleop.connect(calibrate=calibrate) teleop.connect(calibrate=calibrate)
@ -52,6 +53,7 @@ class MultipleUmiTeleop(UFBaseTeleop):
teleop.configure() teleop.configure()
def disconnect(self) -> None: def disconnect(self) -> None:
super().disconnect()
for teleop in self.teleops.values(): for teleop in self.teleops.values():
teleop.disconnect() teleop.disconnect()

View File

@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from dataclasses import dataclass from dataclasses import dataclass, field
from lerobot.teleoperators import TeleoperatorConfig from lerobot.teleoperators import TeleoperatorConfig
from .umi_teleop_config import UmiTeleopConfig from .umi_teleop_config import UmiTeleopConfig
@ -22,7 +22,9 @@ from .umi_teleop_config import UmiTeleopConfig
@TeleoperatorConfig.register_subclass("uf::multiple_umi_teleop") @TeleoperatorConfig.register_subclass("uf::multiple_umi_teleop")
@dataclass @dataclass
class MultipleUmiTeleopConfig(TeleoperatorConfig): class MultipleUmiTeleopConfig(TeleoperatorConfig):
teleops: dict[str, UmiTeleopConfig] teleops: dict[str, TeleoperatorConfig] = field(
default_factory=lambda: {}
)
def __post_init__(self): def __post_init__(self):
self.id = 'multiple_umi_teleop' if self.id is None else self.id self.id = 'multiple_umi_teleop' if self.id is None else self.id

View File

@ -3,8 +3,8 @@
import math import math
from typing import Any from typing import Any
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations from lerobot_robot_ufactory.devices.umi.vive_tracker.transformations import Transformations
from ufactory_lerobot.devices.umi.vive_tracker import ViveTracker from lerobot_robot_ufactory.devices.umi.vive_tracker import ViveTracker
from ..base_teleop import UFBaseTeleop from ..base_teleop import UFBaseTeleop
from .umi_teleop_config import UmiTeleopConfig from .umi_teleop_config import UmiTeleopConfig
@ -26,7 +26,7 @@ class UmiTeleop(UFBaseTeleop):
self.tracker = None self.tracker = None
self.xvlib = None self.xvlib = None
# from ufactory_lerobot.devices.umi.xvlib import XVLib # from lerobot_robot_ufactory.devices.umi.xvlib import XVLib
# self.tracker = ViveTracker() if self.config.use_vive_tracker else None # self.tracker = ViveTracker() if self.config.use_vive_tracker else None
# self.xvlib = XVLib(self.config.serial_number, not self.config.use_vive_tracker, self.config.use_gripper) # self.xvlib = XVLib(self.config.serial_number, not self.config.use_vive_tracker, self.config.use_gripper)
@ -84,7 +84,7 @@ class UmiTeleop(UFBaseTeleop):
pass pass
def connect(self, calibrate: bool = False) -> None: def connect(self, calibrate: bool = False) -> None:
from ufactory_lerobot.devices.umi.xvlib import XVLib from lerobot_robot_ufactory.devices.umi.xvlib import XVLib
self.tracker = ViveTracker() if self.config.use_vive_tracker else None self.tracker = ViveTracker() if self.config.use_vive_tracker else None
self.xvlib = XVLib(self.config.serial_number, not self.config.use_vive_tracker, self.config.use_gripper) self.xvlib = XVLib(self.config.serial_number, not self.config.use_vive_tracker, self.config.use_gripper)
if not self.config.use_vive_tracker: if not self.config.use_vive_tracker:
@ -92,8 +92,10 @@ class UmiTeleop(UFBaseTeleop):
if self.config.use_gripper: if self.config.use_gripper:
self.xvlib.xv_clamp_stream_init() self.xvlib.xv_clamp_stream_init()
self._is_connected = True self._is_connected = True
super().connect(calibrate)
def disconnect(self): def disconnect(self):
super().disconnect()
if self.xvlib: if self.xvlib:
self.xvlib.xv_uninit() self.xvlib.xv_uninit()
self._is_connected = False self._is_connected = False

View File

@ -30,4 +30,4 @@ class UmiTeleopConfig(TeleoperatorConfig):
robot_base_pose: Tuple[float, ...] = (300, 0, 300, 180, -90, 0) # [x, y, z, roll(°), pitch(°), yaw(°)] robot_base_pose: Tuple[float, ...] = (300, 0, 300, 180, -90, 0) # [x, y, z, roll(°), pitch(°), yaw(°)]
def __post_init__(self): def __post_init__(self):
self.id = 'umi_teleop' if self.id is None else self.id self.id = self.serial_number if self.id is None else self.id