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
*.so
*.so.*
!src/ufactory_lerobot/devices/umi/xvlib/lib*.so
!src/ufactory_lerobot/devices/umi/xvlib/lib*.so.*
!src/lerobot_robot_ufactory/devices/umi/xvlib/lib*.so
!src/lerobot_robot_ufactory/devices/umi/xvlib/lib*.so.*
*.egg
*.egg-info/
dist/

View File

@ -1,5 +1,5 @@
RobotConfig:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig
robot:
type: uf::robot
id: "uf_robot"
robot_dof: 5
control_space: "joint"
@ -7,20 +7,9 @@ RobotConfig:
gripper_type: 1
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!
TeleoperatorConfig:
_target_: ufactory_lerobot.teleoperators.gello_teleop.GelloTeleopConfig
teleop:
type: uf::gello_teleop
port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0"
joint_ids: [1, 2, 4, 6, 7]
joint_signs: [1, 1, -1, 1, 1]
@ -28,3 +17,12 @@ TeleoperatorConfig:
gripper_id: 8
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:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig
robot:
type: uf::robot
id: "uf_robot"
robot_dof: 6
control_space: "joint"
@ -7,20 +7,9 @@ RobotConfig:
gripper_type: 1
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!
TeleoperatorConfig:
_target_: ufactory_lerobot.teleoperators.gello_teleop.GelloTeleopConfig
teleop:
type: uf::gello_teleop
port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0"
joint_ids: [1, 2, 4, 5, 6, 7]
joint_signs: [1, 1, -1, 1, 1, 1]
@ -28,3 +17,12 @@ TeleoperatorConfig:
gripper_id: 8
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:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig
robot:
type: uf::robot
id: "uf_robot"
robot_dof: 7
control_space: "joint"
@ -7,21 +7,19 @@ RobotConfig:
gripper_type: 1
start_joints: [0, 0, 0, 90, 0, 90, 0]
DatasetRecordConfig:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
# make sure to edit with your correct configurations!
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: "/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
reset_time_s: 20 # time for resetting env between episodes
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:
_target_: ufactory_lerobot.robots.uf_mock_robot.UFMockRobotConfig
robot:
type: uf::mock_robot
id: "uf_mock_robot"
control_space: "cartesian"
gripper_type: 2
state_offset_action: 3
teleop_id: "pika_xxxx"
cameras:
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"
width: 640
height: 480
fps: 30
fourcc: "MJPG"
DatasetRecordConfig:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
# make sure to edit with your correct configurations!
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: "/home/uf/Data/lerobot_datas/record/ufactory/pika_datas"
repo_id: "ufactory/pika_datas"
@ -24,11 +32,3 @@ DatasetRecordConfig:
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!
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:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig
robot:
type: uf::robot
id: "uf_robot"
robot_dof: 7
control_space: "cartesian"
@ -8,18 +8,24 @@ RobotConfig:
max_linear_velocity: 200
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]
cameras:
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"
width: 640
height: 480
fps: 30
fourcc: "MJPG"
DatasetRecordConfig:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
# make sure to edit with your correct configurations!
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: "/home/uf/Data/lerobot_datas/record/ufactory/xarm7_pika_datas"
repo_id: "ufactory/xarm7_pika_datas"
@ -28,11 +34,3 @@ DatasetRecordConfig:
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!
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:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig
robot:
type: uf::robot
id: "uf_robot"
robot_dof: 7
control_space: "cartesian"
@ -8,20 +8,25 @@ RobotConfig:
cameras:
overhead:
_target_: lerobot.cameras.realsense.configuration_realsense.RealSenseCameraConfig
type: intelrealsense
serial_number_or_name: "Intel RealSense D435I"
width: 640
height: 480
fps: 30
tool:
_target_: lerobot.cameras.realsense.configuration_realsense.RealSenseCameraConfig
type: intelrealsense
serial_number_or_name: "Intel RealSense D435"
width: 640
height: 480
fps: 30
DatasetRecordConfig:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
# space mouse
teleop:
type: uf::spacemouse_teleop
max_value: 300
use_gripper: False
dataset:
# root: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/ufactory/xarm7_pushT"
repo_id: "ufactory/xarm7_pushT"
@ -30,9 +35,3 @@ DatasetRecordConfig:
episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # (invalid) time for resetting env between episodes
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:
_target_: ufactory_lerobot.robots.uf_mock_robot.MultipleUFMockRobotConfig
robot:
type: uf::multiple_mock_robot
id: "multiple_uf_mock_robots"
robots:
left:
_target_: ufactory_lerobot.robots.uf_mock_robot.UFMockRobotConfig
type: uf::mock_robot
id: "left_uf_mock_robot"
control_space: "cartesian"
gripper_type: 1
state_offset_action: 3
teleop_id: "umi_250801DR48FP26001318"
cameras:
fisheye:
_target_: ufactory_lerobot.cameras.umi_camera.configuration_umi.UmiCameraConfig
type: uf::umi_camera
serial_number: "250801DR48FP26001318"
width: 1280
height: 1280
fps: 30
right:
_target_: ufactory_lerobot.robots.uf_mock_robot.UFMockRobotConfig
type: uf::mock_robot
id: "right_uf_mock_robot"
control_space: "cartesian"
gripper_type: 1
state_offset_action: 3
teleop_id: "umi_250801DR48FP26001295"
cameras:
fisheye:
_target_: ufactory_lerobot.cameras.umi_camera.configuration_umi.UmiCameraConfig
type: uf::umi_camera
serial_number: "250801DR48FP26001295"
width: 1280
height: 1280
fps: 30
# make sure to edit with your correct configurations!
TeleoperatorConfig:
_target_: ufactory_lerobot.teleoperators.umi_teleop.MultipleUmiTeleopConfig
teleop:
type: uf::multiple_umi_teleop
id: "multiple_teleops"
teleops:
left:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig
id: "left_teleop"
type: uf::umi_teleop
id: "umi_250801DR48FP26001318"
serial_number: "250801DR48FP26001318"
use_vive_tracker: False
vive_tracker_id: "LHR-555DC7BF"
@ -44,8 +46,8 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 150, -90, 0, 0]
right:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig
id: "right_teleop"
type: uf::umi_teleop
id: "umi_250801DR48FP26001295"
serial_number: "250801DR48FP26001295"
use_vive_tracker: False
vive_tracker_id: "LHR-2425BAD3"
@ -53,15 +55,14 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 150, 90, 0, 0]
DatasetRecordConfig:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
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."
fps: 60
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
push_to_hub: False

View File

@ -1,12 +1,12 @@
RobotConfig:
_target_: ufactory_lerobot.robots.uf_robot.MultipleUFRobotConfig
robot:
type: uf::multiple_robot
id: "multiple_uf_robots"
async_connect: True
async_configure: True
async_action: False
robots:
left:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig
type: uf::robot
id: "left_uf_robot"
robot_dof: 6
control_space: "cartesian"
@ -15,16 +15,23 @@ RobotConfig:
max_linear_velocity: 200
start_joints: [-14.7, 25.6, -49.6, -282.5, 95.9, -65.9]
start_tcp_pose: [400, 0, 150, -90, 0, 0]
teleop_id: "umi_250801DR48FP26001318"
# no_action: True
cameras:
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"
width: 1280
height: 1280
fps: 100
fourcc: "YUYV"
right:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig
type: uf::robot
id: "right_uf_robot"
robot_dof: 6
control_space: "cartesian"
@ -33,9 +40,16 @@ RobotConfig:
max_linear_velocity: 200
start_joints: [14.5, 25.5, -49.6, -76.8, 95.9, 66.6]
start_tcp_pose: [400, 0, 150, 90, 0, 0]
teleop_id: "umi_250801DR48FP26001295"
# no_action: True
cameras:
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"
width: 1280
height: 1280
@ -43,13 +57,13 @@ RobotConfig:
fourcc: "YUYV"
# make sure to edit with your correct configurations!
TeleoperatorConfig:
_target_: ufactory_lerobot.teleoperators.umi_teleop.MultipleUmiTeleopConfig
teleop:
type: uf::multiple_umi_teleop
id: "multiple_teleops"
teleops:
left:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig
id: "left_teleop"
type: uf::umi_teleop
id: "umi_250801DR48FP26001318"
serial_number: "250801DR48FP26001318"
use_vive_tracker: False
vive_tracker_id: "LHR-555DC7BF"
@ -57,8 +71,8 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 150, -90, 0, 0]
right:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig
id: "right_teleop"
type: uf::umi_teleop
id: "umi_250801DR48FP26001295"
serial_number: "250801DR48FP26001295"
use_vive_tracker: False
vive_tracker_id: "LHR-2425BAD3"
@ -66,15 +80,14 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 150, 90, 0, 0]
DatasetRecordConfig:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/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."
fps: 60
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
push_to_hub: False

View File

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

View File

@ -1,5 +1,5 @@
RobotConfig:
_target_: ufactory_lerobot.robots.uf_robot.UFRobotConfig
robot:
type: uf::robot
id: "uf_robot"
robot_dof: 6
control_space: "cartesian"
@ -10,15 +10,15 @@ RobotConfig:
start_tcp_pose: [400, 0, 400, 180, 0, 0]
cameras:
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"
width: 1280
height: 1280
fps: 100
fourcc: "YUYV"
TeleoperatorConfig:
_target_: ufactory_lerobot.teleoperators.umi_teleop.UmiTeleopConfig
teleop:
type: uf::umi_teleop
serial_number: "250801DR48FP26001295"
use_vive_tracker: False
vive_tracker_id: "LHR-2425BAD3"
@ -26,14 +26,13 @@ TeleoperatorConfig:
tracker_to_robot_eef: [0, 0, 0, 0, 0, -90]
robot_base_pose: [400, 0, 400, 180, 0, 0]
DatasetRecordConfig:
_target_: lerobot.scripts.lerobot_record.DatasetRecordConfig
dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)
root: "/home/uf/Data/lerobot_datas/record/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."
fps: 60
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
push_to_hub: False

View File

@ -3,14 +3,14 @@ requires = ["setuptools>=68.0", "wheel>=0.42"]
build-backend = "setuptools.build_meta"
[project]
name = "ufactory_lerobot"
name = "lerobot_robot_ufactory"
version = "0.1.0"
description = "UFACTORY 机械臂与 LeRobot 框架集成"
readme = "README.md"
license = { text = "Apache-2.0" }
requires-python = ">=3.10"
authors = [
{ name = "ufactory_lerobot Contributors" },
{ name = "lerobot_robot_ufactory Contributors" },
]
keywords = ["robotics", "lerobot", "ufactory", "xarm", "imitation-learning"]
classifiers = [
@ -32,11 +32,11 @@ dependencies = [
]
[project.scripts]
uf-robot-teleop = "ufactory_lerobot.scripts.uf_robot_teleop:main"
uf-lerobot-record = "ufactory_lerobot.scripts.uf_lerobot_record:main"
uf-lerobot-eval = "ufactory_lerobot.scripts.uf_lerobot_eval:main"
uf-vive-calibrate = "ufactory_lerobot.scripts.vive_calibrate:main"
uf-camera-view = "ufactory_lerobot.scripts.uf_camera_view:main"
uf-robot-teleop = "lerobot_robot_ufactory.scripts.uf_robot_teleop:main"
uf-lerobot-record = "lerobot_robot_ufactory.scripts.uf_lerobot_record:main"
uf-lerobot-eval = "lerobot_robot_ufactory.scripts.uf_lerobot_eval:main"
uf-vive-calibrate = "lerobot_robot_ufactory.scripts.vive_calibrate:main"
uf-camera-view = "lerobot_robot_ufactory.scripts.uf_camera_view:main"
[project.optional-dependencies]
# GELLO 遥操作
@ -64,7 +64,7 @@ include-package-data = true
where = ["src"]
[tool.setuptools.package-data]
"ufactory_lerobot.devices.umi.xvlib" = ["*.so", "*.so.*"]
"lerobot_robot_ufactory.devices.umi.xvlib" = ["*.so", "*.so.*"]
[tool.ruff]
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 ufactory_lerobot.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.cameras.utils import make_cameras_from_configs as _uf_make_cameras_from_configs
from lerobot_robot_ufactory.robots.utils import make_robot_from_config as _uf_make_robot_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.robots as _lerobot_robot
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_teleoperators_utils.make_teleoperator_from_config = _uf_make_teleoperator_from_config
# register plugin
import ufactory_lerobot.cameras.umi_camera
import ufactory_lerobot.robots.uf_robot
import ufactory_lerobot.robots.uf_mock_robot
import ufactory_lerobot.teleoperators.uf_mock_teleop
import ufactory_lerobot.teleoperators.gello_teleop
import ufactory_lerobot.teleoperators.pika_teleop
import ufactory_lerobot.teleoperators.space_mouse
import ufactory_lerobot.teleoperators.umi_teleop
import lerobot_robot_ufactory.cameras.umi_camera
import lerobot_robot_ufactory.robots.uf_robot
import lerobot_robot_ufactory.robots.uf_mock_robot
import lerobot_robot_ufactory.teleoperators.uf_mock_teleop
import lerobot_robot_ufactory.teleoperators.gello_teleop
import lerobot_robot_ufactory.teleoperators.pika_teleop
import lerobot_robot_ufactory.teleoperators.space_mouse
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.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.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_gripper_port = None
# _lock = threading.Lock()
PIKA_DEVICE_MAP = {}
def __init__(self, dev_type=1, **kwargs):
"""
@ -144,6 +145,9 @@ class PikaDevice(object):
if self._dev_type not in [1, 3]:
return 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
# 初始化Sense对象
self._pika_sense = Sense(port=self._pika_sense_port)
@ -152,6 +156,7 @@ class PikaDevice(object):
logger.error('连接Pika Sense设备失败')
exit(1)
logger.info('Pika Sense设备连接成功')
self.PIKA_DEVICE_MAP[self._pika_sense_port] = self._pika_sense # 注册共享
# 配置Vive Tracker可选
# 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]:
return None
if self._pika_gripper is None:
if self._dev_type in [2, 3]:
from pika.gripper import Gripper
self._pika_gripper = Gripper(port=self._pika_gripper_port)
# 连接设备
if not self._pika_gripper.connect():
logger.error('连接Pika Gripper设备失败')
if self._dev_type in [1, 3]:
self.pika_sense.disconnect()
exit(1)
logger.info('Pika Gripper设备连接成功')
if self._pika_gripper_port in self.PIKA_DEVICE_MAP:
self._pika_gripper = self.PIKA_DEVICE_MAP[self._pika_gripper_port]
return self._pika_gripper
from pika.gripper import Gripper
self._pika_gripper = Gripper(port=self._pika_gripper_port)
self.PIKA_DEVICE_MAP[self._pika_gripper_port] = self._pika_gripper # 注册共享
# 连接设备
if not self._pika_gripper.connect():
logger.error('连接Pika Gripper设备失败')
if self._dev_type in [1, 3]:
self.pika_sense.disconnect()
exit(1)
logger.info('Pika Gripper设备连接成功')
return self._pika_gripper

View File

@ -15,60 +15,61 @@ class MultipleUFMockRobot(Robot):
def __init__(self, config: MultipleUFMockRobotConfig):
super().__init__(config)
self.config = config
self.keys = []
self.robots = []
self.robots: dict[str, UFMockRobot] = {}
for key, robot_config in self.config.robots.items():
self.keys.append(key)
self.robots.append(UFMockRobot(robot_config, prefix=key))
self.robots[key] = UFMockRobot(robot_config, prefix=key)
self.cameras = {}
for robot in self.robots.values():
self.cameras.update(robot.cameras)
@property
def observation_features(self) -> dict:
observation_features = {}
for robot in self.robots:
for robot in self.robots.values():
observation_features.update(robot.observation_features)
return observation_features
@property
def action_features(self) -> dict:
action_features = {}
for robot in self.robots:
for robot in self.robots.values():
action_features.update(robot.action_features)
return action_features
@property
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
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:
for robot in self.robots:
for robot in self.robots.values():
robot.connect(calibrate=calibrate)
def calibrate(self) -> None:
for robot in self.robots:
for robot in self.robots.values():
robot.calibrate()
def configure(self) -> None:
for robot in self.robots:
for robot in self.robots.values():
robot.configure()
def disconnect(self) -> None:
for robot in self.robots:
for robot in self.robots.values():
robot.disconnect()
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()
for obs in observations:
combined_observation.update(obs)
return combined_observation
def send_action(self, action: RobotAction) -> RobotAction:
for i in range(len(self.keys)):
key = self.keys[i]
for key, robot in self.robots.items():
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

View File

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

View File

@ -1,6 +1,7 @@
import numpy as np
from lerobot.cameras.utils import make_cameras_from_configs
from lerobot.robots import Robot
from lerobot_robot_ufactory.context import get_active_teleop
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._is_connected = False
self._is_calibrated =True
self._teleop = self.config.teleop
self._cache_num = self.config.state_offset_action
self._teleop_actions = []
@ -98,7 +98,8 @@ class UFMockRobot(Robot):
pass # CHECK! currently No-op
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
for cam_key, cam in self.cameras.items():
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")
@dataclass
class UFMockRobotConfig(RobotConfig):
teleop_id: str
cameras: dict[str, CameraConfig] = field(
default_factory=lambda: {}
)
@ -13,7 +14,6 @@ class UFMockRobotConfig(RobotConfig):
control_space: str = "joint"
gripper_type: int = 1 # 1: xArm Gripper, 10: Pika Gripper
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
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 .uf_robot_config import UFRobotConfig
@RobotConfig.register_subclass("uf::multiple_robot")
@dataclass
class MultipleUFRobotConfig(RobotConfig):
robots: dict[str, UFRobotConfig]
robots: dict[str, RobotConfig] = field(
default_factory=lambda: {}
)
async_connect: bool = True
async_configure: bool = True
async_action: bool = False

View File

@ -10,7 +10,7 @@ from dataclasses import dataclass
from threading import Thread, Event, Lock
from lerobot.robots import Robot
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 xarm.wrapper import XArmAPI
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 logging
import time
import math
import torch
from pathlib import Path
from dataclasses import asdict, dataclass
from pprint import pformat
from contextlib import nullcontext
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.datasets.pipeline_features import aggregate_pipeline_dataset_features, create_initial_features
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.scripts.lerobot_record import DatasetRecordConfig
from lerobot.datasets.lerobot_dataset import LeRobotDataset
from ufactory_lerobot.utils.utils import instantiate_from_dict, init_keyboard_listener
from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations
from lerobot_robot_ufactory.utils.utils import init_keyboard_listener
from lerobot_robot_ufactory.devices.umi.vive_tracker.transformations import Transformations
def continuous_rotvec(new_rv, prev_rv):
@ -98,6 +96,7 @@ def compute_target_axis_angle(rot_prev, rot_delta):
class EvalConfig:
robot: RobotConfig
dataset: DatasetRecordConfig
teleop: dict = None # no use
# Whether to control the robot with a policy
policy: PreTrainedConfig | None = None
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:
listener.stop()
@parser.wrap()
def get_cfg(cfg: EvalConfig) -> EvalConfig:
return cfg
def main():
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('--rx_continuous', action='store_true', help='rx continuous or not')
args = parser.parse_args()
try:
with open(args.config, '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)
eval_cfg = EvalConfig(robot=config["RobotConfig"], dataset=config["DatasetRecordConfig"])
eval_loop(eval_cfg, args.relative, args.rx_continuous)
args, unknown = parser.parse_known_args()
sys.argv = [sys.argv[0]] + unknown
register_third_party_plugins()
cfg = get_cfg()
eval_loop(cfg, args.relative, args.rx_continuous)
if __name__ == "__main__":

View File

@ -1,4 +1,4 @@
import yaml
import sys
import copy
import time
import queue
@ -7,11 +7,11 @@ import logging
import shutil
import threading
from pathlib import Path
import ufactory_lerobot # patch
import lerobot_robot_ufactory # patch
from lerobot.scripts.lerobot_record import *
from ufactory_lerobot.teleoperators.uf_mock_teleop import UFMockTeleop
from ufactory_lerobot.teleoperators.base_teleop import UFBaseTeleop
from ufactory_lerobot.utils.utils import instantiate_from_dict, init_keyboard_listener
from lerobot_robot_ufactory.teleoperators.uf_mock_teleop import UFMockTeleop
from lerobot_robot_ufactory.teleoperators.base_teleop import UFBaseTeleop
from lerobot_robot_ufactory.utils.utils import init_keyboard_listener
def _get_dataset_writer(dataset):
@ -344,17 +344,14 @@ def record_loop(
timestamp = time.perf_counter() - start_episode_t
@parser.wrap()
def record(cfg: RecordConfig, async_save: bool = False) -> LeRobotDataset:
init_logging()
logging.info(pformat(asdict(cfg)))
if cfg.display_data:
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)
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()
@ -592,31 +589,28 @@ def record(cfg: RecordConfig, async_save: bool = False) -> LeRobotDataset:
log_say("Exiting", cfg.play_sounds)
return dataset
@parser.wrap()
def get_cfg(cfg: RecordConfig) -> RecordConfig:
return cfg
def main():
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('-r', '--resume',
parser.add_argument('-r',
action='store_true', # specify --resume if resume needs to be True
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',
default=False,
help='Enable async background saving (default: False)')
args = parser.parse_args()
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)
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)
args, unknown = parser.parse_known_args()
sys.argv = [sys.argv[0]] + unknown
register_third_party_plugins()
cfg = get_cfg()
if args.r:
cfg.resume = True
cfg.play_sounds = False
record(cfg, async_save=args.async_save)
if __name__ == "__main__":

View File

@ -1,11 +1,11 @@
import yaml
import sys
import argparse
import logging
import time
from pathlib import Path
from dataclasses import asdict, dataclass
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.processor import (
make_default_processors,
@ -23,14 +23,16 @@ from lerobot.utils.robot_utils import precise_sleep
from lerobot.utils.utils import (
init_logging,
)
from ufactory_lerobot.utils.utils import instantiate_from_dict, is_headless, init_keyboard_listener
from ufactory_lerobot.teleoperators.base_teleop import UFBaseTeleop
from lerobot.configs import parser
from lerobot_robot_ufactory.utils.utils import is_headless, init_keyboard_listener
from lerobot_robot_ufactory.teleoperators.base_teleop import UFBaseTeleop
@dataclass
class TeleopConfig:
robot: RobotConfig
teleop: TeleoperatorConfig
dataset: dict = None # no use
fps: int = 30
@ -167,24 +169,17 @@ def teleop_loop(cfg: TeleopConfig):
if is_evt and listener is not None:
listener.stop()
@parser.wrap()
def get_cfg(cfg: TeleopConfig) -> TeleopConfig:
return cfg
def main():
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('-f', '--fps', type=int, default=30,
help='control loop frequency in Hz (default: 30)')
args = parser.parse_args()
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)
args, unknown = parser.parse_known_args()
sys.argv = [sys.argv[0]] + unknown
register_third_party_plugins()
cfg = get_cfg()
teleop_loop(cfg)
if __name__ == "__main__":

View File

@ -1,4 +1,5 @@
from lerobot.teleoperators import Teleoperator
from lerobot_robot_ufactory.context import register_teleop, unregister_teleop
class UFBaseTeleop(Teleoperator):
@ -9,6 +10,12 @@ class UFBaseTeleop(Teleoperator):
super().__init__(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):
"""
启用/停用遥操作

View File

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

View File

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

View File

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

View File

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

View File

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