feat(robot): 添加 uFactory 机械臂完整功能包
包含机器人控制(uf_robot)、遥操作(teleoperators)、 摄像头(cameras)、设备驱动(devices)和执行脚本(scripts)等模块。
This commit is contained in:
parent
c3028aa7b4
commit
4467e19322
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,7 +4,7 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
# *.so
|
||||
*.egg
|
||||
*.egg-info/
|
||||
dist/
|
||||
@ -83,3 +83,4 @@ models/
|
||||
*.onnx
|
||||
*.engine
|
||||
*.trt
|
||||
third_party/
|
||||
|
||||
@ -47,7 +47,7 @@ train = [
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
where = ["."]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
16
ufactory_lerobot/__init__.py
Normal file
16
ufactory_lerobot/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
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
|
||||
import lerobot.cameras as _lerobot_cameras
|
||||
import lerobot.robots as _lerobot_robot
|
||||
import lerobot.teleoperators as _lerobot_teleoperators
|
||||
import lerobot.cameras.utils as _lerobot_cameras_utils
|
||||
import lerobot.robots.utils as _lerobot_robot_utils
|
||||
import lerobot.teleoperators.utils as _lerobot_teleoperators_utils
|
||||
# patch
|
||||
_lerobot_cameras.make_cameras_from_configs = _uf_make_cameras_from_configs
|
||||
_lerobot_robot.make_robot_from_config = _uf_make_robot_from_config
|
||||
_lerobot_teleoperators.make_teleoperator_from_config = _uf_make_teleoperator_from_config
|
||||
_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
|
||||
0
ufactory_lerobot/cameras/__init__.py
Normal file
0
ufactory_lerobot/cameras/__init__.py
Normal file
16
ufactory_lerobot/cameras/umi_camera/__init__.py
Normal file
16
ufactory_lerobot/cameras/umi_camera/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .camera_umi import UmiCamera
|
||||
from .configuration_umi import UmiCameraConfig
|
||||
130
ufactory_lerobot/cameras/umi_camera/camera_umi.py
Normal file
130
ufactory_lerobot/cameras/umi_camera/camera_umi.py
Normal file
@ -0,0 +1,130 @@
|
||||
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Provides the RealSenseCamera class for capturing frames from Intel RealSense cameras.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from threading import Event, Lock, Thread
|
||||
from typing import Any
|
||||
|
||||
import cv2 # type: ignore # TODO: add type stubs for OpenCV
|
||||
import numpy as np # type: ignore # TODO: add type stubs for numpy
|
||||
from numpy.typing import NDArray # type: ignore # TODO: add type stubs for numpy.typing
|
||||
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
|
||||
from lerobot.cameras.camera import Camera
|
||||
from ufactory_lerobot.devices.umi.xvlib import XVLib
|
||||
from .configuration_umi import UmiCameraConfig
|
||||
from lerobot.cameras.configs import ColorMode
|
||||
from lerobot.cameras.utils import get_cv2_rotation
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UmiCamera(Camera):
|
||||
def __init__(self, config: UmiCameraConfig):
|
||||
"""
|
||||
Initializes the RealSenseCamera instance.
|
||||
|
||||
Args:
|
||||
config: The configuration settings for the camera.
|
||||
"""
|
||||
|
||||
super().__init__(config)
|
||||
|
||||
self.config = config
|
||||
self.serial_number = self.config.serial_number
|
||||
|
||||
self.fps = config.fps if config.fps else 30
|
||||
self.width = config.width if config.width else 1280
|
||||
self.height = config.height if config.height else 1280
|
||||
self.color_mode = config.color_mode
|
||||
self.use_depth = config.use_depth
|
||||
self.warmup_s = config.warmup_s
|
||||
self.rotation: int | None = get_cv2_rotation(config.rotation)
|
||||
|
||||
self.last_frame = None
|
||||
self.xvlib = XVLib(self.serial_number)
|
||||
self.xvlib.xv_color_camera_init()
|
||||
self.xvlib.xv_set_color_camera_framerate(self.config.fps)
|
||||
self.frame_lock = Lock()
|
||||
self.new_frame_event: Event = Event()
|
||||
self.thread = Thread(target=self._read_loop, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.serial_number})"
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def find_cameras() -> list[dict[str, Any]]:
|
||||
return []
|
||||
|
||||
def connect(self, warmup: bool = True) -> None:
|
||||
self.xvlib.xv_color_camera_init()
|
||||
if warmup:
|
||||
start_time = time.monotonic()
|
||||
while time.monotonic() - start_time < self.warmup_s:
|
||||
time.sleep(0.1)
|
||||
|
||||
def _read_loop(self):
|
||||
while True:
|
||||
try:
|
||||
frame = self.read()
|
||||
if frame is not None:
|
||||
with self.frame_lock:
|
||||
self.last_frame = frame
|
||||
self.new_frame_event.set()
|
||||
except Exception as e:
|
||||
print('Read Frame Ex: {}'.format(e))
|
||||
time.sleep(0.01)
|
||||
|
||||
def read(self, color_mode = None):
|
||||
ret, img_data = self.xvlib.xv_get_color_image_rgb_data()
|
||||
if ret <= 0:
|
||||
return None
|
||||
requested_color_mode = self.color_mode if color_mode is None else color_mode
|
||||
if requested_color_mode not in (ColorMode.RGB, ColorMode.BGR):
|
||||
raise ValueError(
|
||||
f"Invalid color mode '{requested_color_mode}'. Expected {ColorMode.RGB} or {ColorMode.BGR}."
|
||||
)
|
||||
if requested_color_mode == ColorMode.RGB:
|
||||
frame = img_data.frame(rgb=True)
|
||||
else:
|
||||
frame = img_data.frame(rgb=False)
|
||||
if self.rotation in [cv2.ROTATE_90_CLOCKWISE, cv2.ROTATE_90_COUNTERCLOCKWISE, cv2.ROTATE_180]:
|
||||
frame = cv2.rotate(frame, self.rotation)
|
||||
return frame
|
||||
|
||||
def async_read(self, timeout_ms: float = 200):
|
||||
if not self.new_frame_event.wait(timeout=timeout_ms / 1000.0):
|
||||
thread_alive = self.thread is not None and self.thread.is_alive()
|
||||
raise TimeoutError(
|
||||
f"Timed out waiting for frame from camera {self} after {timeout_ms} ms. "
|
||||
f"Read thread alive: {thread_alive}."
|
||||
)
|
||||
|
||||
with self.frame_lock:
|
||||
frame = self.last_frame
|
||||
self.new_frame_event.clear()
|
||||
return frame
|
||||
|
||||
def disconnect(self) -> None:
|
||||
pass
|
||||
48
ufactory_lerobot/cameras/umi_camera/configuration_umi.py
Normal file
48
ufactory_lerobot/cameras/umi_camera/configuration_umi.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from lerobot.cameras.configs import CameraConfig, ColorMode, Cv2Rotation
|
||||
|
||||
|
||||
@CameraConfig.register_subclass("uf::umi_camera")
|
||||
@dataclass
|
||||
class UmiCameraConfig(CameraConfig):
|
||||
serial_number: str
|
||||
color_mode: ColorMode = ColorMode.RGB
|
||||
use_depth: bool = False
|
||||
rotation: Cv2Rotation = Cv2Rotation.NO_ROTATION
|
||||
warmup_s: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.color_mode not in (ColorMode.RGB, ColorMode.BGR):
|
||||
raise ValueError(
|
||||
f"`color_mode` is expected to be {ColorMode.RGB.value} or {ColorMode.BGR.value}, but {self.color_mode} is provided."
|
||||
)
|
||||
|
||||
if self.rotation not in (
|
||||
Cv2Rotation.NO_ROTATION,
|
||||
Cv2Rotation.ROTATE_90,
|
||||
Cv2Rotation.ROTATE_180,
|
||||
Cv2Rotation.ROTATE_270,
|
||||
):
|
||||
raise ValueError(
|
||||
f"`rotation` is expected to be in {(Cv2Rotation.NO_ROTATION, Cv2Rotation.ROTATE_90, Cv2Rotation.ROTATE_180, Cv2Rotation.ROTATE_270)}, but {self.rotation} is provided."
|
||||
)
|
||||
|
||||
values = (self.fps, self.width, self.height)
|
||||
if any(v is not None for v in values) and any(v is None for v in values):
|
||||
raise ValueError(
|
||||
"For `fps`, `width` and `height`, either all of them need to be set, or none of them."
|
||||
)
|
||||
38
ufactory_lerobot/cameras/utils.py
Normal file
38
ufactory_lerobot/cameras/utils.py
Normal file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import cast
|
||||
from lerobot.utils.import_utils import make_device_from_device_class
|
||||
from lerobot.cameras.utils import make_cameras_from_configs as lerobot_make_cameras_from_configs
|
||||
from lerobot.cameras.camera import Camera
|
||||
from lerobot.cameras.configs import CameraConfig
|
||||
|
||||
|
||||
def make_cameras_from_configs(camera_configs: dict[str, CameraConfig]) -> dict[str, Camera]:
|
||||
lerobot_camera_configs = {key: cfg for key, cfg in camera_configs.items() if not cfg.type.startswith("uf::")}
|
||||
uf_camera_configs = {key: cfg for key, cfg in camera_configs.items() if cfg.type.startswith("uf::")}
|
||||
cameras = lerobot_make_cameras_from_configs(lerobot_camera_configs)
|
||||
|
||||
for key, cfg in uf_camera_configs.items():
|
||||
if cfg.type == "uf::umi_camera":
|
||||
from .umi_camera import UmiCamera
|
||||
cameras[key] = UmiCamera(cfg)
|
||||
else:
|
||||
try:
|
||||
cameras[key] = cast(Camera, make_device_from_device_class(cfg))
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error creating camera with config {cfg}: {e}") from e
|
||||
return cameras
|
||||
0
ufactory_lerobot/devices/__init__.py
Normal file
0
ufactory_lerobot/devices/__init__.py
Normal file
1
ufactory_lerobot/devices/pika/__init__.py
Normal file
1
ufactory_lerobot/devices/pika/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .pika_device import PikaDevice
|
||||
227
ufactory_lerobot/devices/pika/pika_device.py
Normal file
227
ufactory_lerobot/devices/pika/pika_device.py
Normal file
@ -0,0 +1,227 @@
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
import serial
|
||||
from serial.tools import list_ports
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger('pika_device')
|
||||
|
||||
|
||||
def get_serial_ports(vidpid='1a86:7522'):
|
||||
"""
|
||||
搜索所有指定vidpid的串口
|
||||
vidpid: 指定设备的VID:PID字符串, 默认值为'1a86:7522'
|
||||
返回找到的所有符合的串口号列表
|
||||
"""
|
||||
ports = list_ports.comports()
|
||||
pika_ports = []
|
||||
for port in ports:
|
||||
if port.vid is not None and port.pid is not None:
|
||||
if '{:04x}:{:04x}'.format(port.vid, port.pid) == vidpid:
|
||||
pika_ports.append(port.device)
|
||||
# else:
|
||||
# print('pidvid:', '{:04x}:{:04x}'.format(port.vid, port.pid))
|
||||
return pika_ports
|
||||
|
||||
def check_pika_device(port):
|
||||
"""
|
||||
检测串口对应的Pika设备类型
|
||||
返回值:
|
||||
-1: 无法打开串口
|
||||
0: 不是Pika设备
|
||||
1: Pika Sense设备
|
||||
2: Pika Gripper设备
|
||||
"""
|
||||
try:
|
||||
ser = serial.Serial(
|
||||
port=port,
|
||||
baudrate=460800,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
timeout=1.0
|
||||
)
|
||||
time.sleep(0.5) # 等待串口稳定
|
||||
data = b''
|
||||
expired_time = time.monotonic() + 1.0 # 最多等待1秒
|
||||
while time.monotonic() < expired_time:
|
||||
if ser.in_waiting > 0:
|
||||
data += ser.read(ser.in_waiting)
|
||||
if len(data) > 200: # 足够的数据来判断
|
||||
break
|
||||
time.sleep(0.05)
|
||||
ser.close()
|
||||
data_str = data.decode('utf-8', errors='ignore')
|
||||
if '"Command"' in data_str or '"AS5047"' in data_str or '"IMU"' in data_str:
|
||||
# logger.info('✓ 检测到 Pika Sense 设备: {}'.format(port))
|
||||
return 1
|
||||
elif '"motor"' in data_str or '"motorstatus"' in data_str:
|
||||
# logger.info('✓ 检测到 Pika Gripper 设备: {}'.format(port))
|
||||
return 2
|
||||
else:
|
||||
# logger.info('✗ 未检测到 Pika 设备: {}, 数据长度: {}'.format(port, len(data)))
|
||||
return 0
|
||||
except:
|
||||
pass
|
||||
return -1
|
||||
|
||||
|
||||
class PikaDevice(object):
|
||||
# _instance = None
|
||||
# _pika_sense_port = None
|
||||
# _pika_gripper_port = None
|
||||
# _lock = threading.Lock()
|
||||
|
||||
def __init__(self, dev_type=1, **kwargs):
|
||||
"""
|
||||
port: serial port
|
||||
dev_type: 1: sense, 2: gripper
|
||||
"""
|
||||
if dev_type not in [1, 2, 3]:
|
||||
raise ValueError('不支持dev_type={}'.format(dev_type))
|
||||
|
||||
self._dev_type = dev_type
|
||||
self._pika_sense_port = kwargs.get('pika_sense_port', None)
|
||||
self._pika_gripper_port = kwargs.get('pika_gripper_port', None)
|
||||
|
||||
use_pika_sense = self._dev_type in [1, 3]
|
||||
use_pika_gripper = self._dev_type in [2, 3]
|
||||
|
||||
self._pika_sense = None
|
||||
self._pika_gripper = None
|
||||
|
||||
if (use_pika_sense and self._pika_sense_port is None) or (use_pika_gripper and self._pika_gripper_port is None):
|
||||
pika_ports = get_serial_ports()
|
||||
if not pika_ports:
|
||||
logger.error('未找到Pika设备, 请检查连接')
|
||||
exit(1)
|
||||
|
||||
for port in pika_ports:
|
||||
device_type = check_pika_device(port)
|
||||
if device_type == 1 and use_pika_sense and self._pika_sense_port is None:
|
||||
self._pika_sense_port = port
|
||||
logger.info('✓ 检测到 Pika Sense 设备: {}'.format(port))
|
||||
if not use_pika_gripper:
|
||||
break
|
||||
if device_type == 2 and use_pika_gripper and self._pika_gripper_port is None:
|
||||
self._pika_gripper_port = port
|
||||
logger.info('✓ 检测到 Pika Gripper 设备: {}'.format(port))
|
||||
if not use_pika_sense:
|
||||
break
|
||||
|
||||
if use_pika_sense and self._pika_sense_port is None:
|
||||
logger.error('未找到Pika Sense设备, 请检查连接')
|
||||
exit(1)
|
||||
|
||||
if use_pika_gripper and self._pika_gripper_port is None:
|
||||
logger.error('未找到Pika Gripper设备, 请检查连接')
|
||||
exit(1)
|
||||
|
||||
if use_pika_sense:
|
||||
print('Pika Sense设备:', self._pika_sense_port)
|
||||
if use_pika_gripper:
|
||||
print('Pika Gripper 设备:', self._pika_gripper_port)
|
||||
|
||||
self.pika_tracker_device = None
|
||||
|
||||
# def __new__(cls, *args, **kwargs):
|
||||
# if not cls._instance:
|
||||
# with cls._lock:
|
||||
# if not cls._instance:
|
||||
# cls._instance = super().__new__(cls)
|
||||
# cls._instance.init(*args, *kwargs)
|
||||
# return cls._instance
|
||||
|
||||
def __del__(self):
|
||||
if self._pika_sense:
|
||||
self._pika_sense.disconnect()
|
||||
if self._pika_gripper:
|
||||
self._pika_gripper.disconnect()
|
||||
|
||||
@property
|
||||
def pika_sense(self):
|
||||
if self._dev_type not in [1, 3]:
|
||||
return None
|
||||
if self._pika_sense is None:
|
||||
from pika.sense import Sense
|
||||
# 初始化Sense对象
|
||||
self._pika_sense = Sense(port=self._pika_sense_port)
|
||||
# 连接设备
|
||||
if not self._pika_sense.connect():
|
||||
logger.error('连接Pika Sense设备失败')
|
||||
exit(1)
|
||||
logger.info('Pika Sense设备连接成功')
|
||||
|
||||
# 配置Vive Tracker(可选)
|
||||
# sense.set_vive_tracker_config(config_path='path/to/config', lh_config='lighthouse_config')
|
||||
|
||||
tracker = self._pika_sense.get_vive_tracker()
|
||||
if not tracker:
|
||||
logger.error('Vive Tracker初始化失败')
|
||||
self._pika_sense.disconnect()
|
||||
exit(1)
|
||||
logger.info('Vive Tracker初始化成功')
|
||||
time.sleep(2)
|
||||
|
||||
devices = self._pika_sense.get_tracker_devices()
|
||||
if not devices:
|
||||
logger.error('未检测到Vive Tracker设备')
|
||||
self._pika_sense.disconnect()
|
||||
exit(1)
|
||||
logger.info('检测到Vive Tracker设备: {}'.format(devices))
|
||||
|
||||
self.pika_tracker_device = None
|
||||
for device in devices:
|
||||
if device.startswith('WM'):
|
||||
self.pika_tracker_device = device
|
||||
break
|
||||
else:
|
||||
self.pika_tracker_device = devices[0]
|
||||
logger.info('开始跟踪设备: {}\n'.format(self.pika_tracker_device))
|
||||
return self._pika_sense
|
||||
|
||||
@property
|
||||
def pika_gripper(self):
|
||||
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设备连接成功')
|
||||
return self._pika_gripper
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pika_device1 = PikaDevice(1)
|
||||
pika_device1.pika_sense
|
||||
pika_device1.pika_gripper
|
||||
time.sleep(3)
|
||||
|
||||
# input('=================')
|
||||
|
||||
pika_device2 = PikaDevice(2)
|
||||
pika_device2.pika_sense
|
||||
pika_device2.pika_gripper
|
||||
|
||||
input('=================')
|
||||
|
||||
print(pika_device1)
|
||||
print(pika_device1.pika_sense)
|
||||
print(pika_device1.pika_gripper)
|
||||
|
||||
print(pika_device2)
|
||||
print(pika_device2.pika_sense)
|
||||
print(pika_device2.pika_gripper)
|
||||
|
||||
input('=================')
|
||||
|
||||
|
||||
2
ufactory_lerobot/devices/umi/__init__.py
Normal file
2
ufactory_lerobot/devices/umi/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .xvlib import XVLib
|
||||
from .vive_tracker import Transformations, ViveTracker
|
||||
2
ufactory_lerobot/devices/umi/vive_tracker/__init__.py
Normal file
2
ufactory_lerobot/devices/umi/vive_tracker/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .transformations import Transformations
|
||||
from .vive_tracker import ViveTracker
|
||||
248
ufactory_lerobot/devices/umi/vive_tracker/transformations.py
Normal file
248
ufactory_lerobot/devices/umi/vive_tracker/transformations.py
Normal file
@ -0,0 +1,248 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Transformations:
|
||||
@staticmethod
|
||||
def quaternion_to_rotation_matrix(q):
|
||||
"""
|
||||
将四元数转换为旋转矩阵
|
||||
|
||||
注: 四元素顺序为xyzw
|
||||
"""
|
||||
norm = np.linalg.norm(q)
|
||||
if norm < 1e-6:
|
||||
raise ValueError('零四元数无法归一化')
|
||||
|
||||
x, y, z, w = q / norm # 归一化
|
||||
xx, yy, zz = x * x, y * y, z * z
|
||||
xy, xz, yz = x * y, x * z, y * z
|
||||
wx, wy, wz = w * x, w * y, w * z
|
||||
|
||||
R = np.array([
|
||||
[1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy)],
|
||||
[ 2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx)],
|
||||
[ 2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy)]
|
||||
])
|
||||
return R
|
||||
|
||||
@staticmethod
|
||||
def rotation_matrix_to_quaternion(R):
|
||||
"""
|
||||
将3x3变换矩阵转换四元数
|
||||
注: 四元素顺序为xyzw
|
||||
"""
|
||||
# 提取旋转矩阵部分
|
||||
rot_matrix = R[:3, :3]
|
||||
|
||||
# 计算四元数
|
||||
trace = rot_matrix[0, 0] + rot_matrix[1, 1] + rot_matrix[2, 2]
|
||||
|
||||
if trace > 0:
|
||||
s = 0.5 / np.sqrt(trace + 1.0)
|
||||
qw = 0.25 / s
|
||||
qx = (rot_matrix[2, 1] - rot_matrix[1, 2]) * s
|
||||
qy = (rot_matrix[0, 2] - rot_matrix[2, 0]) * s
|
||||
qz = (rot_matrix[1, 0] - rot_matrix[0, 1]) * s
|
||||
elif rot_matrix[0, 0] > rot_matrix[1, 1] and rot_matrix[0, 0] > rot_matrix[2, 2]:
|
||||
s = 2.0 * np.sqrt(1.0 + rot_matrix[0, 0] - rot_matrix[1, 1] - rot_matrix[2, 2])
|
||||
qw = (rot_matrix[2, 1] - rot_matrix[1, 2]) / s
|
||||
qx = 0.25 * s
|
||||
qy = (rot_matrix[0, 1] + rot_matrix[1, 0]) / s
|
||||
qz = (rot_matrix[0, 2] + rot_matrix[2, 0]) / s
|
||||
elif rot_matrix[1, 1] > rot_matrix[2, 2]:
|
||||
s = 2.0 * np.sqrt(1.0 + rot_matrix[1, 1] - rot_matrix[0, 0] - rot_matrix[2, 2])
|
||||
qw = (rot_matrix[0, 2] - rot_matrix[2, 0]) / s
|
||||
qx = (rot_matrix[0, 1] + rot_matrix[1, 0]) / s
|
||||
qy = 0.25 * s
|
||||
qz = (rot_matrix[1, 2] + rot_matrix[2, 1]) / s
|
||||
else:
|
||||
s = 2.0 * np.sqrt(1.0 + rot_matrix[2, 2] - rot_matrix[0, 0] - rot_matrix[1, 1])
|
||||
qw = (rot_matrix[1, 0] - rot_matrix[0, 1]) / s
|
||||
qx = (rot_matrix[0, 2] + rot_matrix[2, 0]) / s
|
||||
qy = (rot_matrix[1, 2] + rot_matrix[2, 1]) / s
|
||||
qz = 0.25 * s
|
||||
|
||||
return [qx, qy, qz, qw]
|
||||
|
||||
@staticmethod
|
||||
def rpy_to_rotation_matrix(roll, pitch, yaw):
|
||||
"""RPY角到旋转矩阵的转换"""
|
||||
cr, sr = np.cos(roll), np.sin(roll)
|
||||
cp, sp = np.cos(pitch), np.sin(pitch)
|
||||
cy, sy = np.cos(yaw), np.sin(yaw)
|
||||
|
||||
R = np.array([
|
||||
[cp*cy, -cr*sy + sr*sp*cy, sr*sy + cr*sp*cy],
|
||||
[cp*sy, cr*cy + sr*sp*sy, -sr*cy + cr*sp*sy],
|
||||
[ -sp, sr*cp, cr*cp],
|
||||
])
|
||||
|
||||
return R
|
||||
|
||||
@staticmethod
|
||||
def rotation_matrix_to_rpy(R, yaw_zero=True):
|
||||
"""
|
||||
旋转矩阵到RPY角的转换
|
||||
|
||||
yaw_zero: 万向节锁情况下, True就把yaw置0, False就把roll置0
|
||||
返回: roll, pitch, yaw
|
||||
"""
|
||||
epsilon = 1e-6
|
||||
if abs(R[2, 0]) > 1 - epsilon: # 万向节锁(pitch=±90°)
|
||||
pitch = np.arcsin(-R[2, 0])
|
||||
roll_yaw = np.arctan2(-R[0, 1], R[1, 1])
|
||||
if yaw_zero:
|
||||
# 保留roll, 把yaw置0
|
||||
roll, yaw = roll_yaw, 0
|
||||
else:
|
||||
# 保留yaw, 把roll置0
|
||||
roll, yaw = 0, roll_yaw
|
||||
else:
|
||||
roll = np.arctan2(R[2, 1], R[2, 2])
|
||||
pitch = np.arcsin(-R[2, 0])
|
||||
yaw = np.arctan2(R[1, 0], R[0, 0])
|
||||
|
||||
return roll, pitch, yaw
|
||||
|
||||
@staticmethod
|
||||
def rxryrz_to_matrix(axis_angle):
|
||||
"""
|
||||
将轴角向量 (rx, ry, rz) 转换为 3x3 旋转矩阵。
|
||||
输入: np.array([rx, ry, rz])
|
||||
- 方向: 旋转轴
|
||||
- 模长: 旋转角度 (弧度)
|
||||
"""
|
||||
theta = np.linalg.norm(axis_angle)
|
||||
|
||||
# 如果角度接近0,返回单位矩阵
|
||||
if theta < 1e-8:
|
||||
return np.eye(3)
|
||||
|
||||
# 归一化旋转轴
|
||||
axis = axis_angle / theta
|
||||
x, y, z = axis
|
||||
|
||||
c = np.cos(theta)
|
||||
s = np.sin(theta)
|
||||
t = 1 - c
|
||||
|
||||
# 罗德里格斯旋转公式
|
||||
# R = I + sin(theta)*K + (1-cos(theta))*K^2
|
||||
# 展开为矩阵形式:
|
||||
R = np.array([
|
||||
[t*x*x + c, t*x*y - s*z, t*x*z + s*y],
|
||||
[t*x*y + s*z, t*y*y + c, t*y*z - s*x],
|
||||
[t*x*z - s*y, t*y*z + s*x, t*z*z + c]
|
||||
])
|
||||
|
||||
return R
|
||||
|
||||
@staticmethod
|
||||
def rotation_matrix_to_rxryrz(R):
|
||||
"""
|
||||
旋转矩阵到轴角的转换 (rx, ry, rz = aixs * angle)
|
||||
返回: rx, ry, rz
|
||||
"""
|
||||
R = np.asarray(R)
|
||||
if R.shape[-2:] != (3, 3):
|
||||
raise ValueError("Input must be (..., 3, 3)")
|
||||
|
||||
# 计算旋转角度 theta
|
||||
trace = np.trace(R)
|
||||
cos_theta = (trace - 1) / 2.0
|
||||
cos_theta = np.clip(cos_theta, -1.0, 1.0) # 防止数值误差导致 arccos 越界
|
||||
theta = np.arccos(cos_theta)
|
||||
eps = 1e-8
|
||||
|
||||
# 情况 1: 无旋转 (theta ≈ 0)
|
||||
if theta < eps:
|
||||
axis = np.array([1.0, 0.0, 0.0])
|
||||
return axis * 0.0
|
||||
|
||||
# 情况 2: 旋转角度接近 pi (180 度)
|
||||
if np.pi - theta < eps:
|
||||
# 此时 sin(theta) ≈ 0,不能用反对称公式
|
||||
# 从 R 对角线提取轴:R = I + 2 * (uu^T - I) => uu^T = (R + I)/2
|
||||
# 所以 u_i^2 = (R_ii + 1)/2
|
||||
diag = np.diag(R)
|
||||
axis = np.sqrt(np.maximum(diag + 1, 0)) # 取非负根
|
||||
|
||||
# 确定符号:利用非对角元素,例如 R[0,1] = 2*u0*u1
|
||||
if axis[0] > eps:
|
||||
if R[0, 1] < 0:
|
||||
axis[1] *= -1
|
||||
if R[0, 2] < 0:
|
||||
axis[2] *= -1
|
||||
elif axis[1] > eps:
|
||||
if R[1, 2] < 0:
|
||||
axis[2] *= -1
|
||||
# 注意:可能存在符号歧义,但旋转效果相同
|
||||
|
||||
axis = axis / np.linalg.norm(axis)
|
||||
return axis * theta
|
||||
|
||||
# 情况 3: 一般情况 (0 < theta < pi)
|
||||
sin_theta = np.sin(theta)
|
||||
axis = np.array([
|
||||
R[2, 1] - R[1, 2],
|
||||
R[0, 2] - R[2, 0],
|
||||
R[1, 0] - R[0, 1]
|
||||
]) / (2 * sin_theta)
|
||||
|
||||
axis = axis / np.linalg.norm(axis) # 确保单位长度(数值误差可能破坏)
|
||||
return axis * theta
|
||||
|
||||
@classmethod
|
||||
def xyzq_to_rotation_matrix(cls, x, y, z, q):
|
||||
T = np.eye(4)
|
||||
T[:3, :3] = cls.quaternion_to_rotation_matrix(q)
|
||||
T[:3, 3] = [x, y, z]
|
||||
return T
|
||||
|
||||
@classmethod
|
||||
def xyzrpy_to_rotation_matrix(cls, x, y, z, roll, pitch, yaw):
|
||||
"""构造4x4齐次变换矩阵"""
|
||||
T = np.eye(4)
|
||||
T[:3, :3] = cls.rpy_to_rotation_matrix(roll, pitch, yaw)
|
||||
T[:3, 3] = [x, y, z]
|
||||
return T
|
||||
|
||||
@classmethod
|
||||
def rotation_matrix_to_xyzq(cls, rotation_matrix):
|
||||
"""从4x4齐次变换矩阵到xyzq的转换"""
|
||||
x, y, z = rotation_matrix[0, 3], rotation_matrix[1, 3], rotation_matrix[2, 3]
|
||||
q = cls.rotation_matrix_to_quaternion(rotation_matrix[:3, :3])
|
||||
return [x, y, z, q]
|
||||
|
||||
@classmethod
|
||||
def rotation_matrix_to_xyzrpy(cls, rotation_matrix):
|
||||
"""从4x4齐次变换矩阵到xyzrpy的转换"""
|
||||
x, y, z = rotation_matrix[0, 3], rotation_matrix[1, 3], rotation_matrix[2, 3]
|
||||
roll, pitch, yaw = cls.rotation_matrix_to_rpy(rotation_matrix)
|
||||
return [x, y, z, roll, pitch, yaw]
|
||||
|
||||
@classmethod
|
||||
def rotation_matrix_to_xyzrxryrz(cls, rotation_matrix):
|
||||
"""从4x4齐次变换矩阵到xyzrxryrz的转换"""
|
||||
x, y, z = rotation_matrix[0, 3], rotation_matrix[1, 3], rotation_matrix[2, 3]
|
||||
rx, ry, rz = cls.rotation_matrix_to_rxryrz(rotation_matrix[:3,:3])
|
||||
return [x, y, z, rx, ry, rz]
|
||||
|
||||
@classmethod
|
||||
def tracker_pose_to_robot_matrix(cls, x, y, z, q, tracker_to_robot_matrix):
|
||||
# Tracker位置对应的变换矩阵
|
||||
tracker_matrix = cls.xyzq_to_rotation_matrix(x, y, z, q)
|
||||
# Tracker位置转换到机械臂坐标系后对应的变换矩阵
|
||||
robot_matrix = np.dot(tracker_matrix, tracker_to_robot_matrix)
|
||||
return robot_matrix
|
||||
|
||||
@classmethod
|
||||
def tracker_robot_matrix_to_robot_pose(cls, begin_tracker_robot_matrix, end_tracker_robot_matrix, robot_base_matrix, is_axis_angle=False):
|
||||
# 机械臂目标位置对应的变换矩阵
|
||||
# 机械臂目标 = 机械臂初始位置 + (当前手姿 - 初始手姿)
|
||||
delta_matrix = np.dot(np.linalg.inv(begin_tracker_robot_matrix), end_tracker_robot_matrix)
|
||||
robot_martix = np.dot(robot_base_matrix, delta_matrix)
|
||||
if is_axis_angle:
|
||||
return cls.rotation_matrix_to_xyzrxryrz(robot_martix)
|
||||
else:
|
||||
return cls.rotation_matrix_to_xyzrpy(robot_martix)
|
||||
234
ufactory_lerobot/devices/umi/vive_tracker/vive_tracker.py
Normal file
234
ufactory_lerobot/devices/umi/vive_tracker/vive_tracker.py
Normal file
@ -0,0 +1,234 @@
|
||||
import sys
|
||||
import ctypes
|
||||
import logging
|
||||
import threading
|
||||
import pysurvive
|
||||
import numpy as np
|
||||
from .transformations import Transformations
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger('uf.vive_tracker')
|
||||
|
||||
|
||||
class Vector(ctypes.Structure):
|
||||
def __getitem__(self, index):
|
||||
# 获取字段名列表
|
||||
field_name = self._fields_[index][0]
|
||||
# 使用 getattr 获取对应属性的值
|
||||
return getattr(self, field_name)
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
# 获取字段名列表
|
||||
field_name = self._fields_[index][0]
|
||||
# 使用 setattr 设置对应属性的值
|
||||
setattr(self, field_name, value)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.to_list(6)}'
|
||||
|
||||
def to_list(self, ndigits=6):
|
||||
return [round(getattr(self, item[0]), ndigits=ndigits) for item in self._fields_]
|
||||
|
||||
|
||||
class Vector3D(Vector):
|
||||
_fields_ = [
|
||||
("x", ctypes.c_double),
|
||||
("y", ctypes.c_double),
|
||||
("z", ctypes.c_double)
|
||||
]
|
||||
|
||||
|
||||
class Vector4D(Vector):
|
||||
_fields_ = [
|
||||
("x", ctypes.c_double),
|
||||
("y", ctypes.c_double),
|
||||
("z", ctypes.c_double),
|
||||
("w", ctypes.c_double)
|
||||
]
|
||||
|
||||
|
||||
class PoseData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("position", Vector3D),
|
||||
# ("orientation", Vector3D),
|
||||
("quaternion", Vector4D),
|
||||
("hostTimestamp", ctypes.c_double),
|
||||
# ("edgeTimestampUs", ctypes.c_longlong),
|
||||
# ("confidence", ctypes.c_double)
|
||||
]
|
||||
|
||||
|
||||
class SingletonMeta(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super().__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class ViveTracker(metaclass=SingletonMeta):
|
||||
# _instance = None
|
||||
# _initialized = False
|
||||
def __init__(self, config_path=None, lh_config=None, args=None):
|
||||
# if self._initialized:
|
||||
# return
|
||||
# self._initialized = True
|
||||
self.config_path = config_path
|
||||
self.lh_config = lh_config
|
||||
self.args = args if args else []
|
||||
self.running = False
|
||||
self.context = None
|
||||
self.collector_thread = None
|
||||
self.data_lock = threading.Lock()
|
||||
self.latest_poses = {}
|
||||
self.latest_raw_poses = {}
|
||||
self.init()
|
||||
|
||||
# def __new__(cls, *args, **kwargs):
|
||||
# if cls._instance is None:
|
||||
# cls._instance = super().__new__(cls)
|
||||
# return cls._instance
|
||||
|
||||
def __del__(self):
|
||||
logger.info("正在停止Vive Tracker位姿追踪...")
|
||||
self.running = False
|
||||
# 等待线程结束
|
||||
if self.collector_thread:
|
||||
self.collector_thread.join(timeout=2.0)
|
||||
# 清理资源
|
||||
self.context = None
|
||||
logger.info("Vive Tracker已断开连接")
|
||||
|
||||
@staticmethod
|
||||
def to_str(v):
|
||||
return v.decode("utf-8") if isinstance(v, bytes) else str(v)
|
||||
|
||||
def list_devices(self):
|
||||
# import pysurvive
|
||||
# for obj in self.context.Objects():
|
||||
# name = self.to_str(obj.Name())
|
||||
# serial_number = None
|
||||
# if hasattr(pysurvive, "simple_serial_number"):
|
||||
# serial_number = self.to_str(pysurvive.simple_serial_number(obj.ptr))
|
||||
# print("object:", name, "serial:", serial_number)
|
||||
return [key for key in self.latest_poses.keys() if not key.startswith('WM')]
|
||||
|
||||
def init(self):
|
||||
# 构建pysurvive参数
|
||||
survive_args = sys.argv[:1] # 保留程序名
|
||||
|
||||
# 添加配置文件参数
|
||||
if self.config_path:
|
||||
survive_args.extend(['--config', self.config_path])
|
||||
|
||||
# 添加灯塔配置参数
|
||||
if self.lh_config:
|
||||
survive_args.extend(['--lh', self.lh_config])
|
||||
|
||||
# 添加其他参数
|
||||
survive_args.extend(self.args)
|
||||
try:
|
||||
logger.info("正在初始化pysurvive...")
|
||||
self.context = pysurvive.SimpleContext(survive_args)
|
||||
if not self.context:
|
||||
logger.error("错误: 无法初始化pysurvive上下文")
|
||||
return False
|
||||
|
||||
logger.info("pysurvive初始化成功")
|
||||
# 标记为运行状态
|
||||
self.running = True
|
||||
|
||||
# 创建并启动位姿收集线程
|
||||
self.collector_thread = threading.Thread(target=self._pose_collector)
|
||||
self.collector_thread.daemon = True
|
||||
self.collector_thread.start()
|
||||
except Exception as e:
|
||||
logger.error(f"连接Vive Tracker时发生错误: {e}")
|
||||
self.running = False
|
||||
return False
|
||||
|
||||
def _pose_collector(self):
|
||||
initial_rotation = Transformations.xyzrpy_to_rotation_matrix(0, 0, 0, -30 / 180.0 * np.pi, 0, 0)
|
||||
|
||||
# alignment_rotation = Transformations.xyzrpy_to_rotation_matrix(0, 0, 0, -np.pi / 2, -np.pi / 2, 0)
|
||||
alignment_rotation = Transformations.xyzrpy_to_rotation_matrix(0, 0, 0, -np.pi / 2, np.pi, np.pi)
|
||||
|
||||
rotate_matrix = np.dot(initial_rotation, alignment_rotation)
|
||||
# 应用平移变换 - 将采集到的pose数据变换到夹爪中心
|
||||
# transform_matrix = Transformations.xyzrpy_to_rotation_matrix(0.172, 0, -0.076, 0, 0, 0)
|
||||
# transform_matrix = Transformations.xyzrpy_to_rotation_matrix(0, 0, 0, 0, 0, 0)
|
||||
|
||||
# tracker_to_robot_matrix = Transformations.xyzrpy_to_rotation_matrix(0, 0, 0, np.pi, 0, np.pi)
|
||||
tracker_to_robot_matrix = Transformations.xyzrpy_to_rotation_matrix(0, 0, 0, 0, 0, -np.pi / 2)
|
||||
|
||||
robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*[0, 0, 0, np.pi, -np.pi / 2, 0])
|
||||
begin_tracker_robot_matrix = None
|
||||
|
||||
cnt = 0
|
||||
|
||||
# 持续获取最新位姿
|
||||
while self.running and self.context.Running():
|
||||
updated = self.context.NextUpdated()
|
||||
if not updated:
|
||||
continue
|
||||
if cnt < 100:
|
||||
cnt += 1
|
||||
continue
|
||||
# 获取设备名称
|
||||
device_name = str(updated.Name(), 'utf-8')
|
||||
serial_number = None
|
||||
if hasattr(pysurvive, "simple_serial_number"):
|
||||
serial_number = self.to_str(pysurvive.simple_serial_number(updated.ptr))
|
||||
# 获取位姿数据
|
||||
pose_obj = updated.Pose()
|
||||
pose_data = pose_obj[0] # 位姿数据
|
||||
timestamp = pose_obj[1] # 时间戳
|
||||
position = [pose_data.Pos[0], pose_data.Pos[1], pose_data.Pos[2]]
|
||||
quaternion = [pose_data.Rot[1], pose_data.Rot[2], pose_data.Rot[3], pose_data.Rot[0]]
|
||||
origin_mat = Transformations.xyzq_to_rotation_matrix(*position, quaternion)
|
||||
# tracker_matrix = np.dot(origin_mat, rotate_matrix)
|
||||
tracker_matrix = np.matmul(origin_mat, rotate_matrix)
|
||||
# tracker_matrix = np.matmul(np.matmul(origin_mat, rotate_matrix), transform_matrix)
|
||||
|
||||
x, y, z, q = Transformations.rotation_matrix_to_xyzq(tracker_matrix)
|
||||
pose_data = PoseData(position=Vector3D(x, y, z), quaternion=Vector4D(*q), hostTimestamp=timestamp)
|
||||
pose_raw_data = PoseData(position=Vector3D(*position), quaternion=Vector4D(*quaternion), hostTimestamp=timestamp)
|
||||
with self.data_lock:
|
||||
self.latest_poses[device_name] = pose_data
|
||||
self.latest_raw_poses[device_name] = pose_raw_data
|
||||
if serial_number:
|
||||
self.latest_poses[serial_number] = pose_data
|
||||
self.latest_raw_poses[serial_number] = pose_raw_data
|
||||
|
||||
# tracker_robot_matrix = np.dot(tracker_matrix, tracker_to_robot_matrix)
|
||||
# if begin_tracker_robot_matrix is None:
|
||||
# begin_tracker_robot_matrix = tracker_robot_matrix
|
||||
# pose = Transformations.tracker_robot_matrix_to_robot_pose(begin_tracker_robot_matrix, tracker_robot_matrix, robot_base_matrix, is_axis_angle=True)
|
||||
|
||||
# pose_data = PoseData(position=Vector3D(*pose[:3]), orientation=Vector3D(*pose[3:]), quaternion=Vector4D(*quaternion), hostTimestamp=timestamp)
|
||||
# with self.data_lock:
|
||||
# self.latest_poses[device_name] = pose_data
|
||||
|
||||
def get_pose(self, device_name=None):
|
||||
if device_name:
|
||||
with self.data_lock:
|
||||
if device_name in self.latest_poses:
|
||||
return self.latest_poses[device_name]
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
with self.data_lock:
|
||||
return self.latest_poses.copy()
|
||||
|
||||
def get_raw_pose(self, device_name=None):
|
||||
if device_name:
|
||||
with self.data_lock:
|
||||
if device_name in self.latest_raw_poses:
|
||||
return self.latest_raw_poses[device_name]
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
with self.data_lock:
|
||||
return self.latest_raw_poses.copy()
|
||||
1
ufactory_lerobot/devices/umi/xvlib/__init__.py
Normal file
1
ufactory_lerobot/devices/umi/xvlib/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .xvlib import XVLib
|
||||
611
ufactory_lerobot/devices/umi/xvlib/xvlib.py
Normal file
611
ufactory_lerobot/devices/umi/xvlib/xvlib.py
Normal file
@ -0,0 +1,611 @@
|
||||
import os
|
||||
import ctypes
|
||||
import cv2
|
||||
import time
|
||||
import logging
|
||||
import numpy as np
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger('uf.xvlib')
|
||||
|
||||
|
||||
class DeviceStruct(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("uuid", ctypes.c_char * 100)
|
||||
]
|
||||
|
||||
@property
|
||||
def serial_number(self):
|
||||
return self.uuid.decode('utf-8')
|
||||
|
||||
|
||||
class Vector(ctypes.Structure):
|
||||
def __getitem__(self, index):
|
||||
# 获取字段名列表
|
||||
field_name = self._fields_[index][0]
|
||||
# 使用 getattr 获取对应属性的值
|
||||
return getattr(self, field_name)
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
# 获取字段名列表
|
||||
field_name = self._fields_[index][0]
|
||||
# 使用 setattr 设置对应属性的值
|
||||
setattr(self, field_name, value)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.to_list(6)}'
|
||||
|
||||
def to_list(self, ndigits=6):
|
||||
return [round(getattr(self, item[0]), ndigits=ndigits) for item in self._fields_]
|
||||
|
||||
|
||||
class Vector3B(Vector):
|
||||
_fields_ = [
|
||||
("x", ctypes.c_bool),
|
||||
("y", ctypes.c_bool),
|
||||
("z", ctypes.c_bool)
|
||||
]
|
||||
|
||||
|
||||
class Vector3D(Vector):
|
||||
_fields_ = [
|
||||
("x", ctypes.c_double),
|
||||
("y", ctypes.c_double),
|
||||
("z", ctypes.c_double)
|
||||
]
|
||||
|
||||
|
||||
class Vector4D(Vector):
|
||||
_fields_ = [
|
||||
("x", ctypes.c_double),
|
||||
("y", ctypes.c_double),
|
||||
("z", ctypes.c_double),
|
||||
("w", ctypes.c_double)
|
||||
]
|
||||
|
||||
|
||||
class ClampData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("timestamp", ctypes.c_double),
|
||||
("data", ctypes.c_double)
|
||||
]
|
||||
|
||||
|
||||
class ColorImageData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("codec", ctypes.c_int),
|
||||
("width", ctypes.c_int),
|
||||
("height", ctypes.c_int),
|
||||
("data", ctypes.c_uint8 * int(1280*1280*3)),
|
||||
("dataSize", ctypes.c_uint),
|
||||
("hostTimestamp", ctypes.c_double),
|
||||
("edgeTimestampUs", ctypes.c_longlong)
|
||||
]
|
||||
def frame(self, rgb=False):
|
||||
np_array = np.frombuffer(bytes(self.data[:self.dataSize]), dtype=np.uint8)
|
||||
if self.codec == 0: # YUYV 格式, 重塑为 (h, w, 2),因为每两个字节包含 Y 和 UV 信息
|
||||
yuv_mat = np_array.reshape((self.height, self.width, 2))
|
||||
if rgb:
|
||||
frame = cv2.cvtColor(yuv_mat, cv2.COLOR_YUV2RGB_YUYV)
|
||||
else:
|
||||
frame = cv2.cvtColor(yuv_mat, cv2.COLOR_YUV2BGR_YUYV)
|
||||
elif self.codec == 1: # YU12 (即 I420) 格式 (UV 平面)
|
||||
yuv_mat = np_array.reshape((int(self.height * 1.5), self.width))
|
||||
if rgb:
|
||||
frame = cv2.cvtColor(yuv_mat, cv2.COLOR_YUV2RGB_I420)
|
||||
else:
|
||||
frame = cv2.cvtColor(yuv_mat, cv2.COLOR_YUV2BGR_I420)
|
||||
elif self.codec == 2: # JPEG 格式, 直接解码,不需要知道宽高(宽高包含在 JPEG 头中,但可以用 w,h 校验)
|
||||
frame = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
|
||||
elif self.codec == 3: # NV12 格式 (UV 交错)
|
||||
yuv_mat = np_array.reshape((int(self.height * 1.5), self.width))
|
||||
if rgb:
|
||||
frame = cv2.cvtColor(yuv_mat, cv2.COLOR_YUV2RGB_NV12)
|
||||
else:
|
||||
frame = cv2.cvtColor(yuv_mat, cv2.COLOR_YUV2BGR_NV12)
|
||||
elif self.codec == 4: # BITSTREAM (H.264/H.265) 格式, 同样使用 imdecode,OpenCV 会自动处理常见的视频流头
|
||||
frame = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
|
||||
else:
|
||||
frame = np_array
|
||||
return frame
|
||||
|
||||
|
||||
class DepthImageData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("type", ctypes.c_int),
|
||||
("width", ctypes.c_int),
|
||||
("height", ctypes.c_int),
|
||||
("confidence", ctypes.c_double),
|
||||
("data", ctypes.c_uint8 * int(1280*1280*3)),
|
||||
("dataSize", ctypes.c_uint),
|
||||
("hostTimestamp", ctypes.c_double),
|
||||
("edgeTimestampUs", ctypes.c_longlong)
|
||||
]
|
||||
def frame(self):
|
||||
np_array = np.frombuffer(bytes(self.data[:self.dataSize]), dtype=np.uint8)
|
||||
if self.type == 0: # Depth_16, 数据大小应为 w * h * 2
|
||||
# 1. 转换为 uint16 类型
|
||||
depth_uint16 = np_array.view(dtype=np.uint16).reshape((self.height, self.width))
|
||||
|
||||
# 2. 归一化用于显示 (0-255)
|
||||
# 深度值通常在 0-65535 (mm),直接显示是全黑的
|
||||
# cv2.normalize 将数据拉伸到 0-255 范围
|
||||
depth_norm = cv2.normalize(depth_uint16, None, 0, 255, cv2.NORM_MINMAX)
|
||||
depth_norm = np.uint8(depth_norm) # 转为 8位灰度图
|
||||
|
||||
# 3. 可选:转为伪彩色以便观察
|
||||
depth_color = cv2.applyColorMap(depth_norm, cv2.COLORMAP_JET)
|
||||
frame = depth_color
|
||||
elif self.type == 1: # Depth_32, 数据大小应为 w * h * 4
|
||||
depth_float = np_array.view(dtype=np.float32).reshape((self.height, self.width))
|
||||
|
||||
# 显示处理:截取有效范围 (例如 0-5米) 并归一化
|
||||
# 注意:这里假设最大值是 5000mm 或 5.0m,根据实际情况调整
|
||||
# max_depth = 5000.0 if np.max(depth_float) > 100 else 5.0
|
||||
depth_norm = cv2.normalize(depth_float, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
|
||||
depth_color = cv2.applyColorMap(depth_norm, cv2.COLORMAP_JET)
|
||||
frame = depth_color
|
||||
elif self.type == 2: # IR, 通常是 8位或16位灰度图, 数据大小可能是 w*h (8bit) 或 w*h*2 (16bit)
|
||||
# 尝试根据数据长度判断位深
|
||||
if len(np_array) == self.width * self.height:
|
||||
ir_img = np_array.reshape((self.height, self.width)) # 8位
|
||||
elif len(np_array) == self.width * self.height * 2:
|
||||
ir_img = np_array.view(dtype=np.uint16).reshape((self.height, self.width)) # 16位
|
||||
# 16位转8位显示
|
||||
ir_img = cv2.normalize(ir_img, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
|
||||
else:
|
||||
raise ValueError("IR 数据长度不匹配")
|
||||
# 转伪彩色
|
||||
ir_color = cv2.applyColorMap(ir_img, cv2.COLORMAP_JET)
|
||||
frame = ir_color
|
||||
elif self.type == 3: # Cloud, 这不是图像,是 xyz 坐标集合, 数据大小应为 w * h * 3 * 4 (float32) 或类似
|
||||
# 假设是 float32 格式
|
||||
cloud_data = np_array.view(dtype=np.float32).reshape((-1, 3))
|
||||
# cloud_data 现在是一个 N x 3 的数组,每一行是 (x, y, z)
|
||||
# 这里不返回图像,返回点云数据供 PCL 或 Open3D 处理
|
||||
frame = cloud_data
|
||||
elif self.type in [4, 5, 6]: # 4: Raw, 5: Eeprom, 6: IQ, 非图像数据,无法直接显示
|
||||
frame = None
|
||||
else:
|
||||
frame = np_array
|
||||
return frame
|
||||
|
||||
|
||||
class RgbImageData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("width", ctypes.c_int),
|
||||
("height", ctypes.c_int),
|
||||
("data", ctypes.c_uint8 * (1280*1280*3)),
|
||||
("dataSize", ctypes.c_uint),
|
||||
("hostTimestamp", ctypes.c_double),
|
||||
("edgeTimestampUs", ctypes.c_longlong)
|
||||
]
|
||||
def frame(self, rgb=False):
|
||||
rgb_frame = np.frombuffer(bytes(self.data[:self.dataSize]), dtype=np.uint8).reshape((self.height, self.width, 3))
|
||||
if rgb:
|
||||
return rgb_frame.copy()
|
||||
# RGB => BGR
|
||||
return cv2.cvtColor(rgb_frame, cv2.COLOR_RGB2BGR) # 转换颜色空间
|
||||
|
||||
|
||||
class GrayScaleImage(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("width", ctypes.c_int),
|
||||
("height", ctypes.c_int),
|
||||
("data", ctypes.c_uint8 * (640*480))
|
||||
]
|
||||
def frame(self):
|
||||
buf_size = 640 * 480
|
||||
count = min(self.width * self.height, buf_size)
|
||||
return np.array(self.data[:count], dtype=np.uint8).reshape((self.height, self.width, 1))
|
||||
|
||||
|
||||
class FisheyeImagesData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("hostTimestamp", ctypes.c_double),
|
||||
("edgeTimestampUs", ctypes.c_longlong),
|
||||
("images", GrayScaleImage * 4),
|
||||
("id", ctypes.c_longlong)
|
||||
]
|
||||
|
||||
def frame(self, inx=-1):
|
||||
inx_vaild = inx >= 0 and inx < 4
|
||||
if inx_vaild:
|
||||
return self.images[inx].frame()
|
||||
else:
|
||||
frame0 = cv2.resize(self.images[0].frame(), (480, 360))
|
||||
frame1 = cv2.resize(self.images[1].frame(), (480, 360))
|
||||
frame2 = cv2.resize(self.images[2].frame(), (480, 360))
|
||||
frame3 = cv2.resize(self.images[3].frame(), (480, 360))
|
||||
|
||||
up_frame = cv2.hconcat([frame0, frame1])
|
||||
down_frame = cv2.hconcat([frame2, frame3])
|
||||
return cv2.vconcat([up_frame, down_frame])
|
||||
|
||||
|
||||
class EyetrackingImageData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("hostTimestamp", ctypes.c_double),
|
||||
("edgeTimestampUs", ctypes.c_longlong),
|
||||
("images", GrayScaleImage * 4),
|
||||
]
|
||||
|
||||
|
||||
class PoseData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("position", Vector3D),
|
||||
("orientation", Vector3D),
|
||||
("quaternion", Vector4D),
|
||||
("hostTimestamp", ctypes.c_double),
|
||||
("edgeTimestampUs", ctypes.c_longlong),
|
||||
("confidence", ctypes.c_double)
|
||||
]
|
||||
|
||||
|
||||
class ImuData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("gyro", Vector3D),
|
||||
("accel", Vector3D),
|
||||
("accelSaturation", Vector3B),
|
||||
("magneto", Vector3D),
|
||||
("temperature", ctypes.c_double),
|
||||
("hostTimestamp", ctypes.c_double),
|
||||
("edgeTimestampUs", ctypes.c_longlong)
|
||||
]
|
||||
|
||||
|
||||
class EventData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("hostTimestamp", ctypes.c_double),
|
||||
("edgeTimestampUs", ctypes.c_longlong),
|
||||
("type", ctypes.c_int),
|
||||
("state", ctypes.c_int)
|
||||
]
|
||||
|
||||
|
||||
class XVLib:
|
||||
_xvlib = None
|
||||
|
||||
def __init__(self, serial_number, init_slam=False, init_clamp_stream=False, init_color_camera=False, init_fisheye_cameras=False):
|
||||
self.instance_id = -1
|
||||
|
||||
self._clamp_data = ClampData()
|
||||
self._color_image_data = ColorImageData()
|
||||
self._color_image_rgb_data = RgbImageData()
|
||||
self._depth_image_data = DepthImageData()
|
||||
self._fisheye_images_data = FisheyeImagesData()
|
||||
self._slam_data = PoseData()
|
||||
self._external_stream_data = PoseData()
|
||||
self._spheretrack_stream_data = PoseData()
|
||||
|
||||
self.xv_get_devices()
|
||||
time.sleep(1)
|
||||
|
||||
serial_number = ctypes.c_char_p(serial_number.encode('utf-8'))
|
||||
self.instance_id = self.xv_init(serial_number, init_slam, init_clamp_stream, init_color_camera, init_fisheye_cameras)
|
||||
if self.instance_id > 0:
|
||||
logger.info('Device initialized successfully.')
|
||||
else:
|
||||
raise Exception('Device initialized failure.')
|
||||
|
||||
def __del__(self):
|
||||
self.xv_uninit()
|
||||
|
||||
@classmethod
|
||||
def __load_library(cls):
|
||||
if cls._xvlib is None:
|
||||
# 加载动态库
|
||||
lib_dir = os.path.dirname(__file__)
|
||||
if os.path.exists(os.path.join(lib_dir, 'libopencv_core.so.4.2')):
|
||||
ctypes.CDLL(os.path.join(lib_dir, 'libopencv_core.so.4.2'), mode=ctypes.RTLD_GLOBAL)
|
||||
if os.path.exists(os.path.join(lib_dir, 'libopencv_imgproc.so.4.2')):
|
||||
ctypes.CDLL(os.path.join(lib_dir, 'libopencv_imgproc.so.4.2'), mode=ctypes.RTLD_GLOBAL)
|
||||
lib_path = os.path.abspath(os.path.join(lib_dir, 'libxvlib.so'))
|
||||
logger.info(f"Loading library from: {lib_path}")
|
||||
cls._xvlib = ctypes.CDLL(lib_path)
|
||||
logger.info('Library initialized successfully.')
|
||||
|
||||
@classmethod
|
||||
def xv_get_devices(cls, max_devices=16):
|
||||
cls.__load_library()
|
||||
devices = (DeviceStruct * max_devices)()
|
||||
device_count = ctypes.c_int(0)
|
||||
cls._xvlib.xv_get_devices(
|
||||
ctypes.byref(devices),
|
||||
ctypes.byref(device_count),
|
||||
ctypes.c_int(max_devices)
|
||||
)
|
||||
return device_count.value, list(devices[:device_count.value])
|
||||
|
||||
def xv_init(self, serial_number, init_slam, init_clamp_stream, init_color_camera, init_fisheye_cameras):
|
||||
return self._xvlib.xv_init(serial_number, init_slam, init_clamp_stream, init_color_camera, init_fisheye_cameras)
|
||||
|
||||
def xv_uninit(self):
|
||||
if self._xvlib is not None and self.instance_id > 0:
|
||||
return self._xvlib.xv_uninit(self.instance_id)
|
||||
else:
|
||||
return -1
|
||||
|
||||
def xv_sleep(self, level = 0):
|
||||
return self._xvlib.xv_sleep(self.instance_id, level)
|
||||
|
||||
def xv_wakeup(self):
|
||||
return self._xvlib.xv_wakeup(self.instance_id)
|
||||
|
||||
def xv_slam_init(self):
|
||||
return self._xvlib.xv_slam_init(self.instance_id)
|
||||
|
||||
def xv_slam_uninit(self):
|
||||
return self._xvlib.xv_slam_uninit(self.instance_id)
|
||||
|
||||
def xv_imu_sensor_init(self):
|
||||
return self._xvlib.xv_imu_sensor_init(self.instance_id)
|
||||
|
||||
def xv_imu_sensor_uninit(self):
|
||||
return self._xvlib.xv_imu_sensor_uninit(self.instance_id)
|
||||
|
||||
def xv_event_stream_init(self):
|
||||
return self._xvlib.xv_event_stream_init(self.instance_id)
|
||||
|
||||
def xv_event_stream_uninit(self):
|
||||
return self._xvlib.xv_event_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_orientation_stream_init(self):
|
||||
return self._xvlib.xv_orientation_stream_init(self.instance_id)
|
||||
|
||||
def xv_orientation_stream_uninit(self):
|
||||
return self._xvlib.xv_orientation_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_fisheye_cameras_init(self):
|
||||
return self._xvlib.xv_fisheye_cameras_init(self.instance_id)
|
||||
|
||||
def xv_fisheye_cameras_uninit(self):
|
||||
return self._xvlib.xv_fisheye_cameras_uninit(self.instance_id)
|
||||
|
||||
def xv_color_camera_init(self):
|
||||
return self._xvlib.xv_color_camera_init(self.instance_id)
|
||||
|
||||
def xv_color_camera_uninit(self):
|
||||
return self._xvlib.xv_color_camera_uninit(self.instance_id)
|
||||
|
||||
def xv_tof_camera_init(self):
|
||||
return self._xvlib.xv_tof_camera_init(self.instance_id)
|
||||
|
||||
def xv_tof_camera_uninit(self):
|
||||
return self._xvlib.xv_tof_camera_uninit(self.instance_id)
|
||||
|
||||
def xv_sgbm_camera_init(self, config):
|
||||
return self._xvlib.xv_sgbm_camera_init(self.instance_id, ctypes.c_char_p(config.encode('utf-8')))
|
||||
|
||||
def xv_sgbm_camera_uninit(self):
|
||||
return self._xvlib.xv_sgbm_camera_uninit(self.instance_id)
|
||||
|
||||
def xv_eyetracking_camera_init(self):
|
||||
return self._xvlib.xv_eyetracking_camera_init(self.instance_id)
|
||||
|
||||
def xv_eyetracking_camera_uninit(self):
|
||||
return self._xvlib.xv_eyetracking_camera_uninit(self.instance_id)
|
||||
|
||||
def xv_gaze_stream_init(self):
|
||||
return self._xvlib.xv_gaze_stream_init(self.instance_id)
|
||||
|
||||
def xv_gaze_stream_uninit(self):
|
||||
return self._xvlib.xv_gaze_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_iris_stream_init(self):
|
||||
return self._xvlib.xv_iris_stream_init(self.instance_id)
|
||||
|
||||
def xv_iris_stream_uninit(self):
|
||||
return self._xvlib.xv_iris_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_gesture_stream_init(self):
|
||||
return self._xvlib.xv_gesture_stream_init(self.instance_id)
|
||||
|
||||
def xv_gesture_stream_uninit(self):
|
||||
return self._xvlib.xv_gesture_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_gps_stream_init(self):
|
||||
return self._xvlib.xv_gps_stream_init(self.instance_id)
|
||||
|
||||
def xv_gps_stream_uninit(self):
|
||||
return self._xvlib.xv_gps_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_gps_distance_stream_init(self):
|
||||
return self._xvlib.xv_gps_distance_stream_init(self.instance_id)
|
||||
|
||||
def xv_gps_distance_stream_uninit(self):
|
||||
return self._xvlib.xv_gps_distance_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_terrestrial_magnetism_stream_init(self):
|
||||
return self._xvlib.xv_terrestrial_magnetism_stream_init(self.instance_id)
|
||||
|
||||
def xv_terrestrial_magnetism_stream_uninit(self):
|
||||
return self._xvlib.xv_terrestrial_magnetism_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_external_stream_init(self):
|
||||
return self._xvlib.xv_external_stream_init(self.instance_id)
|
||||
|
||||
def xv_external_stream_uninit(self):
|
||||
return self._xvlib.xv_external_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_mic_stream_init(self):
|
||||
return self._xvlib.xv_mic_stream_init(self.instance_id)
|
||||
|
||||
def xv_mic_stream_uninit(self):
|
||||
return self._xvlib.xv_mic_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_object_detector_init(self):
|
||||
return self._xvlib.xv_object_detector_init(self.instance_id)
|
||||
|
||||
def xv_object_detector_uninit(self):
|
||||
return self._xvlib.xv_object_detector_uninit(self.instance_id)
|
||||
|
||||
def xv_object_detector_RKNN3588_init(self):
|
||||
return self._xvlib.xv_object_detector_RKNN3588_init(self.instance_id)
|
||||
|
||||
def xv_object_detector_RKNN3588_uninit(self):
|
||||
return self._xvlib.xv_object_detector_RKNN3588_uninit(self.instance_id)
|
||||
|
||||
def xv_device_status_stream_init(self):
|
||||
return self._xvlib.xv_device_status_stream_init(self.instance_id)
|
||||
|
||||
def xv_device_status_stream_uninit(self):
|
||||
return self._xvlib.xv_device_status_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_clamp_stream_init(self):
|
||||
return self._xvlib.xv_clamp_stream_init(self.instance_id)
|
||||
|
||||
def xv_clamp_stream_uninit(self):
|
||||
return self._xvlib.xv_clamp_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_spheretrack_stream_init(self):
|
||||
return self._xvlib.xv_spheretrack_stream_init(self.instance_id)
|
||||
|
||||
def xv_spheretrack_stream_uninit(self):
|
||||
return self._xvlib.xv_spheretrack_stream_uninit(self.instance_id)
|
||||
|
||||
def xv_get_clamp_stream_data(self):
|
||||
ret = self._xvlib.xv_get_clamp_stream_data(self.instance_id, ctypes.byref(self._clamp_data))
|
||||
return ret, self._clamp_data
|
||||
|
||||
def xv_get_color_image_data(self):
|
||||
ret = self._xvlib.xv_get_color_image_data(self.instance_id, ctypes.byref(self._color_image_data))
|
||||
return ret, self._color_image_data
|
||||
|
||||
def xv_get_color_image_rgb_data(self):
|
||||
ret = self._xvlib.xv_get_color_image_rgb_data(self.instance_id, ctypes.byref(self._color_image_rgb_data))
|
||||
return ret, self._color_image_rgb_data
|
||||
|
||||
def xv_get_depth_image_data(self):
|
||||
ret = self._xvlib.xv_get_depth_image_data(self.instance_id, ctypes.byref(self._depth_image_data))
|
||||
return ret, self._depth_image_data
|
||||
|
||||
def xv_get_fisheye_images_data(self, index=5):
|
||||
ret = self._xvlib.xv_get_fisheye_images_data(self.instance_id, ctypes.byref(self._fisheye_images_data), ctypes.c_size_t(index))
|
||||
return ret, self._fisheye_images_data
|
||||
|
||||
def xv_get_slam_data(self):
|
||||
ret = self._xvlib.xv_get_slam_data(self.instance_id, ctypes.byref(self._slam_data))
|
||||
return ret, self._slam_data
|
||||
|
||||
def xv_get_slam_pose(self, prediction):
|
||||
ret = self._xvlib.xv_get_slam_pose(self.instance_id, ctypes.byref(self._slam_data), ctypes.c_double(prediction))
|
||||
return ret, self._slam_data
|
||||
|
||||
def xv_get_slam_pose_at(self, timestamp):
|
||||
ret = self._xvlib.xv_get_slam_pose_at(self.instance_id, ctypes.byref(self._slam_data), ctypes.c_double(timestamp))
|
||||
return ret, self._slam_data
|
||||
|
||||
def xv_get_external_stream_data(self):
|
||||
ret = self._xvlib.xv_get_external_stream_data(self.instance_id, ctypes.byref(self._external_stream_data))
|
||||
return ret, self._external_stream_data
|
||||
|
||||
def xv_get_spheretrack_stream_data(self):
|
||||
ret = self._xvlib.xv_get_spheretrack_stream_data(self.instance_id, ctypes.byref(self._spheretrack_stream_data))
|
||||
return ret, self._spheretrack_stream_data
|
||||
|
||||
def xv_set_color_camera_rgb_mode(self, mode):
|
||||
"""
|
||||
Docstring for xv_set_color_camera_rgb_mode
|
||||
|
||||
:param mode: Description
|
||||
0: AF
|
||||
1: MF
|
||||
2: Unknown
|
||||
"""
|
||||
return self._xvlib.xv_set_color_camera_rgb_mode(self.instance_id, ctypes.c_int(mode))
|
||||
|
||||
def xv_set_color_camera_resolution(self, resolution):
|
||||
"""
|
||||
Docstring for xv_set_color_camera_resolution
|
||||
|
||||
:param resolution:
|
||||
0: RGB_1920x1080
|
||||
1: RGB_1280x720
|
||||
2: RGB_640x480
|
||||
3: RGB_320x240 (not supported now)
|
||||
4: RGB_2560x1920 (not supported now)
|
||||
5: RGB_3840x2160 (not supported now)
|
||||
"""
|
||||
return self._xvlib.xv_set_color_camera_resolution(self.instance_id, ctypes.c_int(resolution))
|
||||
|
||||
def xv_set_color_camera_framerate(self, framerate):
|
||||
return self._xvlib.xv_set_color_camera_framerate(self.instance_id, ctypes.c_float(framerate))
|
||||
|
||||
def xv_set_color_camera_brightness(self, brightness):
|
||||
return self._xvlib.xv_set_color_camera_brightness(self.instance_id, ctypes.c_int(brightness))
|
||||
|
||||
def xv_set_tof_camera_mode(self, mode):
|
||||
return self._xvlib.xv_set_tof_camera_mode(self.instance_id, ctypes.c_int(mode))
|
||||
|
||||
def xv_set_tof_camera_stream_mode(self, mode):
|
||||
"""
|
||||
Docstring for xv_set_tof_camera_stream_mode
|
||||
|
||||
:param mode: Description
|
||||
0: DepthOnly
|
||||
1: CloudOnly
|
||||
2: DepthAndCloud
|
||||
3: None
|
||||
4: CloudOnLeftHandSlam
|
||||
"""
|
||||
return self._xvlib.xv_set_tof_camera_stream_mode(self.instance_id, ctypes.c_int(mode))
|
||||
|
||||
def xv_set_tof_camera_distance_mode(self, mode):
|
||||
"""
|
||||
Docstring for xv_set_tof_camera_distance_mode
|
||||
|
||||
:param mode: Description
|
||||
0: Short
|
||||
1: Middle
|
||||
2: Long
|
||||
"""
|
||||
return self._xvlib.xv_set_tof_camera_distance_mode(self.instance_id, ctypes.c_int(mode))
|
||||
|
||||
def xv_set_tof_camera_resolution(self, resolution):
|
||||
"""
|
||||
Docstring for xv_set_tof_camera_resolution
|
||||
|
||||
:param resolution: Description
|
||||
-1: Unknown
|
||||
0: VGA
|
||||
1: QVGA
|
||||
2: HQVGA
|
||||
"""
|
||||
return self._xvlib.xv_set_tof_camera_resolution(self.instance_id, ctypes.c_int(resolution))
|
||||
|
||||
def xv_set_tof_camera_framerate(self, framerate):
|
||||
"""
|
||||
Docstring for xv_set_tof_camera_framerate
|
||||
|
||||
:param framerate: Description
|
||||
0: FPS_5
|
||||
1: FPS_10
|
||||
2: FPS_15
|
||||
3: FPS_20
|
||||
4: FPS_25
|
||||
5: FPS_30
|
||||
"""
|
||||
return self._xvlib.xv_set_tof_camera_framerate(self.instance_id, ctypes.c_float(framerate))
|
||||
|
||||
def xv_set_tof_camera_brightness(self, brightness):
|
||||
return self._xvlib.xv_set_tof_camera_brightness(self.instance_id, ctypes.c_int(brightness))
|
||||
|
||||
def xv_set_fisheye_cameras_resolution(self, resolution):
|
||||
return self._xvlib.xv_set_fisheye_cameras_resolution(self.instance_id, ctypes.c_int(resolution))
|
||||
|
||||
def xv_set_fisheye_cameras_framerate(self, framerate):
|
||||
return self._xvlib.xv_set_fisheye_cameras_framerate(self.instance_id, ctypes.c_float(framerate))
|
||||
|
||||
def xv_set_fisheye_cameras_brightness(self, brightness):
|
||||
return self._xvlib.xv_set_fisheye_cameras_brightness(self.instance_id, ctypes.c_int(brightness))
|
||||
|
||||
def xv_set_eyetracking_camera_resolution(self, resolution):
|
||||
return self._xvlib.xv_set_eyetracking_camera_resolution(self.instance_id, ctypes.c_int(resolution))
|
||||
|
||||
def xv_set_eyetracking_camera_framerate(self, framerate):
|
||||
return self._xvlib.xv_set_eyetracking_camera_framerate(self.instance_id, ctypes.c_float(framerate))
|
||||
|
||||
def xv_set_eyetracking_camera_brightness(self, brightness):
|
||||
return self._xvlib.xv_set_eyetracking_camera_brightness(self.instance_id, ctypes.c_int(brightness))
|
||||
0
ufactory_lerobot/robots/__init__.py
Normal file
0
ufactory_lerobot/robots/__init__.py
Normal file
20
ufactory_lerobot/robots/uf_mock_robot/__init__.py
Normal file
20
ufactory_lerobot/robots/uf_mock_robot/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2025 UFACTORY Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .uf_mock_robot_config import UFMockRobotConfig
|
||||
from .uf_mock_robot import UFMockRobot
|
||||
from .multiple_uf_mock_robot_config import MultipleUFMockRobotConfig
|
||||
from .multiple_uf_mock_robot import MultipleUFMockRobot
|
||||
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from lerobot.processor.core import RobotAction, RobotObservation
|
||||
from lerobot.processor.core import RobotObservation
|
||||
from lerobot.robots import Robot
|
||||
from .multiple_uf_mock_robot_config import MultipleUFMockRobotConfig
|
||||
from .uf_mock_robot import UFMockRobot
|
||||
|
||||
|
||||
class MultipleUFMockRobot(Robot):
|
||||
|
||||
config_class = MultipleUFMockRobotConfig
|
||||
name = "UFACTORY Multiple Mock Robot"
|
||||
|
||||
def __init__(self, config: MultipleUFMockRobotConfig):
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
self.keys = []
|
||||
self.robots = []
|
||||
for key, robot_config in self.config.robots.items():
|
||||
self.keys.append(key)
|
||||
self.robots.append(UFMockRobot(robot_config, prefix=key))
|
||||
|
||||
@property
|
||||
def observation_features(self) -> dict:
|
||||
observation_features = {}
|
||||
for robot in self.robots:
|
||||
observation_features.update(robot.observation_features)
|
||||
return observation_features
|
||||
|
||||
@property
|
||||
def action_features(self) -> dict:
|
||||
action_features = {}
|
||||
for robot in self.robots:
|
||||
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)
|
||||
|
||||
@property
|
||||
def is_calibrated(self) -> bool:
|
||||
return all(robot.is_calibrated for robot in self.robots)
|
||||
|
||||
def connect(self, calibrate: bool = True) -> None:
|
||||
for robot in self.robots:
|
||||
robot.connect(calibrate=calibrate)
|
||||
|
||||
def calibrate(self) -> None:
|
||||
for robot in self.robots:
|
||||
robot.calibrate()
|
||||
|
||||
def configure(self) -> None:
|
||||
for robot in self.robots:
|
||||
robot.configure()
|
||||
|
||||
def disconnect(self) -> None:
|
||||
for robot in self.robots:
|
||||
robot.disconnect()
|
||||
|
||||
def get_observation(self) -> RobotObservation:
|
||||
observations = [robot.get_observation() for robot in self.robots]
|
||||
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]
|
||||
action_subset = {k: v for k, v in action.items() if k.startswith(f"{key}.")}
|
||||
self.robots[i].send_action(action_subset)
|
||||
return action
|
||||
@ -0,0 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
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]
|
||||
133
ufactory_lerobot/robots/uf_mock_robot/uf_mock_robot.py
Normal file
133
ufactory_lerobot/robots/uf_mock_robot/uf_mock_robot.py
Normal file
@ -0,0 +1,133 @@
|
||||
import numpy as np
|
||||
from lerobot.cameras.utils import make_cameras_from_configs
|
||||
from lerobot.robots import Robot
|
||||
from .uf_mock_robot_config import UFMockRobotConfig
|
||||
|
||||
|
||||
CARTESIAN_OBS_KEYS = [
|
||||
"pose.x", "pose.y", "pose.z", "pose.rx", "pose.ry", "pose.rz",
|
||||
# un-comment if you need more features below:
|
||||
# "velo.x", "velo.y", "velo.z", "velo.rx", "velo.ry", "velo.rz",
|
||||
]
|
||||
|
||||
CARTESIAN_ACTION_KEYS = [
|
||||
"pose.x", "pose.y", "pose.z", "pose.rx", "pose.ry", "pose.rz",
|
||||
]
|
||||
|
||||
|
||||
class UFMockRobot(Robot):
|
||||
|
||||
config_class = UFMockRobotConfig
|
||||
name = "UFACTORY Mock Robot"
|
||||
|
||||
def __init__(self, config: UFMockRobotConfig, prefix=''):
|
||||
super().__init__(config)
|
||||
self.prefix = '' if not prefix else f"{prefix}."
|
||||
self.config = config
|
||||
self._dof = config.robot_dof
|
||||
if self._dof == None or (not self._dof in (5, 6, 7)):
|
||||
raise ValueError(f"Please specify the correct DOF uf_robot!, got {self._dof}")
|
||||
|
||||
self._control_space = self.config.control_space
|
||||
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 = []
|
||||
|
||||
self.cameras = make_cameras_from_configs(config.cameras)
|
||||
|
||||
self._gripper_type = 0 if not self.config.gripper_control else self.config.gripper_type if self.config.gripper_type > 0 else 1
|
||||
|
||||
@property
|
||||
def _robot_state_features(self)-> dict:
|
||||
if self._control_space == "joint":
|
||||
state_features = {f"{self.prefix}J{motor}.pos": float for motor in range(1, self._dof+1)}
|
||||
if self._jnt_obs_has_vel:
|
||||
state_features.update({f"{self.prefix}J{motor}.vel": float for motor in range(1, self._dof+1)})
|
||||
if self._gripper_type > 0:
|
||||
state_features.update({f"{self.prefix}gripper.pos": float})
|
||||
elif self._control_space == "cartesian":
|
||||
state_features = {f"{self.prefix}{key}": float for key in CARTESIAN_OBS_KEYS}
|
||||
if self._gripper_type > 0:
|
||||
state_features.update({f"{self.prefix}gripper.pos": float})
|
||||
else:
|
||||
raise ValueError(f"Please check the given control space of uf_robot! got {self._control_space}")
|
||||
return state_features
|
||||
|
||||
@property
|
||||
# CHECK!! channel first or last?
|
||||
def _cam_features(self) -> dict:
|
||||
cam_ft = {}
|
||||
for cam_key, cam in self.cameras.items():
|
||||
cam_ft[f"{self.prefix}{cam_key}"] = (cam.height, cam.width, 3)
|
||||
return cam_ft
|
||||
|
||||
@property
|
||||
def observation_features(self) -> dict[str, type | tuple]:
|
||||
return {**self._robot_state_features, **self._cam_features}
|
||||
|
||||
@property
|
||||
def action_features(self)-> dict:
|
||||
if self._control_space == "joint":
|
||||
action_ft = {f"{self.prefix}J{motor}.pos": float for motor in range(1, self._dof+1)}
|
||||
elif self._control_space == "cartesian":
|
||||
action_ft = {f"{self.prefix}{key}": float for key in CARTESIAN_ACTION_KEYS}
|
||||
else:
|
||||
raise ValueError(f"Please check the given control space of uf_robot! got {self._control_space}")
|
||||
# Consider adding velocity configuration ??
|
||||
if self._gripper_type > 0:
|
||||
action_ft.update({f"{self.prefix}gripper.pos": float})
|
||||
return action_ft
|
||||
|
||||
def connect(self, calibrate: bool = True) -> None:
|
||||
self._is_connected = True
|
||||
for cam in self.cameras.values():
|
||||
cam.connect()
|
||||
self._is_connected = self._is_connected and cam.is_connected
|
||||
|
||||
self.configure()
|
||||
if calibrate:
|
||||
self.calibrate()
|
||||
|
||||
def configure(self) -> None:
|
||||
pass
|
||||
|
||||
def calibrate(self) -> None:
|
||||
self._is_calibrated = True
|
||||
pass # CHECK! currently No-op
|
||||
|
||||
def get_observation(self) -> dict[str, np.ndarray]:
|
||||
new_act = self._teleop.get_action()
|
||||
# Capture images from cameras
|
||||
for cam_key, cam in self.cameras.items():
|
||||
new_act[f"{self.prefix}{cam_key}"] = cam.async_read()
|
||||
|
||||
# if len(self._teleop_actions) >= self._cache_num:
|
||||
# act = self._teleop_actions.pop(0)
|
||||
# else:
|
||||
# act = new_act
|
||||
# self._teleop_actions.append(new_act)
|
||||
|
||||
return new_act
|
||||
|
||||
def send_action(self, action: dict) -> np.ndarray:
|
||||
return action
|
||||
|
||||
def disconnect(self) -> None:
|
||||
for cam in self.cameras.values():
|
||||
cam.disconnect()
|
||||
|
||||
self._is_connected = False
|
||||
|
||||
def is_calibrated(self) -> bool:
|
||||
"""Whether the robot is currently calibrated or not. Should be always `True` if not applicable"""
|
||||
return self._is_calibrated
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Whether the robot is currently calibrated or not. Should be always `True` if not applicable"""
|
||||
return self._is_connected
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
from dataclasses import dataclass, field
|
||||
from lerobot.cameras import CameraConfig
|
||||
from lerobot.cameras.opencv import OpenCVCameraConfig
|
||||
from lerobot.robots import RobotConfig
|
||||
|
||||
@RobotConfig.register_subclass("uf::mock_robot")
|
||||
@dataclass
|
||||
class UFMockRobotConfig(RobotConfig):
|
||||
# cameras
|
||||
cameras: dict[str, CameraConfig] = field(
|
||||
default_factory=lambda: {
|
||||
"fisheye": OpenCVCameraConfig(
|
||||
index_or_path=6,
|
||||
width=640,
|
||||
height=480,
|
||||
fps=30,
|
||||
fourcc="MJPG"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
robot_dof: int | None = None # Set it correctly if controlling in joint space!
|
||||
control_space: str = "joint"
|
||||
gripper_control: bool = True
|
||||
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
|
||||
20
ufactory_lerobot/robots/uf_robot/__init__.py
Normal file
20
ufactory_lerobot/robots/uf_robot/__init__.py
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2025 UFACTORY Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .uf_robot_config import UFRobotConfig
|
||||
from .uf_robot import UFRobot
|
||||
from .multiple_uf_robot_config import MultipleUFRobotConfig
|
||||
from .multiple_uf_robot import MultipleUFRobot
|
||||
122
ufactory_lerobot/robots/uf_robot/multiple_uf_robot.py
Normal file
122
ufactory_lerobot/robots/uf_robot/multiple_uf_robot.py
Normal file
@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import queue
|
||||
import threading
|
||||
from typing import Any
|
||||
from lerobot.robots import Robot
|
||||
from .multiple_uf_robot_config import MultipleUFRobotConfig
|
||||
from .uf_robot import UFRobot
|
||||
|
||||
|
||||
class MultipleUFRobot(Robot):
|
||||
|
||||
config_class = MultipleUFRobotConfig
|
||||
name = "UFACTORY Multiple Robot"
|
||||
|
||||
def __init__(self, config: MultipleUFRobotConfig):
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
self._is_async_connect = config.async_connect
|
||||
self._is_async_configure = config.async_configure
|
||||
self._is_async_action = config.async_action
|
||||
self.robots = {}
|
||||
self.action_queues = {}
|
||||
self.action_threads = {}
|
||||
for key, robot_config in self.config.robots.items():
|
||||
robot = UFRobot(robot_config, prefix=key)
|
||||
self.robots[key] = robot
|
||||
if self._is_async_action:
|
||||
action_queue = queue.Queue()
|
||||
self.action_queues[key] = action_queue
|
||||
action_thread = threading.Thread(target=self.run_action_loop, args=(action_queue, robot), daemon=True)
|
||||
self.action_threads[key] = action_thread
|
||||
# action_thread.start()
|
||||
self.cameras = {}
|
||||
for robot in self.robots.values():
|
||||
self.cameras.update(robot.cameras)
|
||||
|
||||
def run_action_loop(self, action_queue: queue.Queue, robot: Robot):
|
||||
while robot.is_connected:
|
||||
try:
|
||||
action = action_queue.get(timeout=0.5)
|
||||
if action is None:
|
||||
break
|
||||
robot.send_action(action)
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
@property
|
||||
def observation_features(self) -> dict:
|
||||
observation_features = {}
|
||||
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.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.values())
|
||||
|
||||
@property
|
||||
def is_calibrated(self) -> bool:
|
||||
return all(robot.is_calibrated for robot in self.robots.values())
|
||||
|
||||
def connect(self, calibrate: bool = True) -> None:
|
||||
if self._is_async_connect:
|
||||
threads = []
|
||||
for robot in self.robots.values():
|
||||
thread = threading.Thread(target=robot.connect, kwargs={"calibrate": calibrate}, daemon=True)
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
else:
|
||||
for robot in self.robots.values():
|
||||
robot.connect(calibrate=calibrate)
|
||||
if self._is_async_action:
|
||||
for thread in self.action_threads.values():
|
||||
thread.start()
|
||||
|
||||
def calibrate(self) -> None:
|
||||
for robot in self.robots.values():
|
||||
robot.calibrate()
|
||||
|
||||
def configure(self) -> None:
|
||||
if self._is_async_configure:
|
||||
threads = []
|
||||
for robot in self.robots.values():
|
||||
thread = threading.Thread(target=robot.configure, daemon=True)
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
else:
|
||||
for robot in self.robots.values():
|
||||
robot.configure()
|
||||
|
||||
def disconnect(self) -> None:
|
||||
for robot in self.robots.values():
|
||||
robot.disconnect()
|
||||
|
||||
def get_observation(self) -> dict[str, Any]:
|
||||
observations = {}
|
||||
for robot in self.robots.values():
|
||||
observations.update(robot.get_observation())
|
||||
return observations
|
||||
|
||||
def send_action(self, action: dict[str, Any]) -> dict[str, Any]:
|
||||
if self._is_async_action:
|
||||
for key, robot in self.robots.items():
|
||||
action_subset = {k: v for k, v in action.items() if k.startswith(f"{key}.")}
|
||||
self.action_queues[key].put(action_subset)
|
||||
else:
|
||||
for key, robot in self.robots.items():
|
||||
action_subset = {k: v for k, v in action.items() if k.startswith(f"{key}.")}
|
||||
robot.send_action(action_subset)
|
||||
return action
|
||||
11
ufactory_lerobot/robots/uf_robot/multiple_uf_robot_config.py
Normal file
11
ufactory_lerobot/robots/uf_robot/multiple_uf_robot_config.py
Normal file
@ -0,0 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
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]
|
||||
async_connect: bool = True
|
||||
async_configure: bool = True
|
||||
async_action: bool = False
|
||||
465
ufactory_lerobot/robots/uf_robot/uf_robot.py
Normal file
465
ufactory_lerobot/robots/uf_robot/uf_robot.py
Normal file
@ -0,0 +1,465 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import time
|
||||
import math
|
||||
import logging
|
||||
import struct
|
||||
import numpy as np
|
||||
from enum import IntEnum
|
||||
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 .uf_robot_config import UFRobotConfig
|
||||
from xarm.wrapper import XArmAPI
|
||||
from xarm.core.utils import convert
|
||||
|
||||
## Configurations:
|
||||
INIT_SYNC_JOINT_VELOCITY_RAD = 0.2
|
||||
|
||||
CARTESIAN_OBS_KEYS = [
|
||||
"pose.x", "pose.y", "pose.z", "pose.rx", "pose.ry", "pose.rz",
|
||||
# un-comment if you need more features below:
|
||||
# "velo.x", "velo.y", "velo.z", "velo.rx", "velo.ry", "velo.rz",
|
||||
]
|
||||
|
||||
CARTESIAN_ACTION_KEYS = [
|
||||
"pose.x", "pose.y", "pose.z", "pose.rx", "pose.ry", "pose.rz",
|
||||
]
|
||||
|
||||
class GripperType(IntEnum):
|
||||
NoGripper = 0
|
||||
xArmGripper = 1
|
||||
xArmGripperG2 = 2
|
||||
BioGripperG2 = 3
|
||||
PikaGripper = 10
|
||||
RobotiqGripper = 11
|
||||
|
||||
|
||||
@dataclass
|
||||
class GripperParam:
|
||||
name: str
|
||||
open_pos: int
|
||||
close_pos: int
|
||||
speed: int = 0
|
||||
force: int = 0
|
||||
gripper_norm: float = 0
|
||||
|
||||
def get_grippos(self, gripper_norm):
|
||||
pos = self.open_pos + gripper_norm * (self.close_pos - self.open_pos)
|
||||
min_pos, max_pos = min(self.open_pos, self.close_pos), max(self.open_pos, self.close_pos)
|
||||
return int(min(max(min_pos, pos), max_pos))
|
||||
|
||||
def get_gripper_norm(self, grippos):
|
||||
if grippos is None:
|
||||
return self.gripper_norm
|
||||
self.gripper_norm = (self.open_pos - grippos) / (self.open_pos - self.close_pos)
|
||||
return self.gripper_norm
|
||||
|
||||
|
||||
class UFRobot(Robot, Thread):
|
||||
|
||||
config_class = UFRobotConfig
|
||||
name = "UFACTORY Robot"
|
||||
|
||||
def __init__(self, config: UFRobotConfig, prefix=''):
|
||||
super().__init__(config)
|
||||
Thread.__init__(self)
|
||||
self.prefix = '' if not prefix else f"{prefix}."
|
||||
self.config = config
|
||||
self._dof = config.robot_dof
|
||||
if self._dof == None or (not self._dof in (5,6,7)):
|
||||
raise ValueError(f"Please specify the correct DOF uf_robot!, got {self._dof}")
|
||||
|
||||
self._control_space = self.config.control_space
|
||||
|
||||
self.real_arm = None
|
||||
self.cameras = make_cameras_from_configs(config.cameras)
|
||||
|
||||
self._is_connected = False
|
||||
self._is_calibrated =True
|
||||
|
||||
self.logs = {}
|
||||
|
||||
self._cmd_cnt = 0
|
||||
|
||||
self._max_joint_velocity = math.radians(self.config.max_joint_velocity)
|
||||
self._max_linear_velocity = self.config.max_linear_velocity
|
||||
|
||||
self._start_tcp_pose = self.config.start_tcp_pose
|
||||
self._start_joints = self.config.start_joints
|
||||
|
||||
self.report_stop_event = Event()
|
||||
self._rt_report_normal = False
|
||||
self._update_lock = Lock()
|
||||
self._use_rt_report = (self._control_space == "cartesian") # Cartesian observations must utilize rt_report
|
||||
self._cart_obs_has_vel = any('velo.' in key for key in CARTESIAN_OBS_KEYS)
|
||||
self._jnt_obs_has_vel = self.config.observe_joint_vel
|
||||
|
||||
self._gripper_type = 0 if not self.config.gripper_control else self.config.gripper_type
|
||||
if self._gripper_type == GripperType.xArmGripper:
|
||||
gripper_speed = 5000 if self.config.gripper_speed < 0 else min(max(50, self.config.gripper_speed), 5000)
|
||||
gripper_force = 50 if self.config.gripper_force < 0 else self.config.gripper_force # # not support
|
||||
self._gripper_param = GripperParam('xArmGripper', open_pos=800, close_pos=0, speed=gripper_speed, force=gripper_force)
|
||||
elif self._gripper_type == GripperType.xArmGripperG2:
|
||||
speed = 225 if self.config.gripper_speed < 0 else min(max(15, self.config.gripper_speed), 225)
|
||||
gripper_speed = int(((speed * 60) / 9.88235 + 140) / 0.4)
|
||||
gripper_force = 50 if self.config.gripper_force < 0 else min(max(1, self.config.gripper_force), 100)
|
||||
self._gripper_param = GripperParam('xArmGripperG2', open_pos=84, close_pos=0, speed=gripper_speed, force=gripper_force)
|
||||
elif self._gripper_type == GripperType.BioGripperG2:
|
||||
gripper_speed = 2000 if self.config.gripper_speed < 0 else min(max(500, self.config.gripper_speed), 4500)
|
||||
gripper_force = 100 if self.config.gripper_force < 0 else min(max(1, self.config.gripper_force), 100)
|
||||
self._gripper_param = GripperParam('BioGripperG2', open_pos=150, close_pos=71, speed=gripper_speed, force=gripper_force)
|
||||
elif self._gripper_type == GripperType.PikaGripper:
|
||||
self.pika_device = PikaDevice(2, pika_gripper_port=self.config.gripper_port)
|
||||
self.pika_gripper = self.pika_device.pika_gripper
|
||||
logger = logging.getLogger('pika.gripper')
|
||||
logger.setLevel(logging.WARNING)
|
||||
gripper_speed = 0 if self.config.gripper_speed < 0 else self.config.gripper_speed # not support
|
||||
gripper_force = 0 if self.config.gripper_force < 0 else self.config.gripper_force # not support
|
||||
self._gripper_param = GripperParam('PikaGripper', open_pos=100, close_pos=0, speed=gripper_speed, force=gripper_force)
|
||||
elif self._gripper_type == GripperType.RobotiqGripper:
|
||||
gripper_speed = 255 if self.config.gripper_speed < 0 else min(max(1, self.config.gripper_speed), 255)
|
||||
gripper_force = 255 if self.config.gripper_force < 0 else min(max(1, self.config.gripper_force), 255)
|
||||
self._gripper_param = GripperParam('RobotiqGripper', open_pos=0, close_pos=0xFF, speed=gripper_speed, force=gripper_force)
|
||||
else: # no gripper or not support
|
||||
self._gripper_type = 0
|
||||
self._gripper_param = GripperParam('NoGripper', open_pos=0, close_pos=0, speed=0, force=0)
|
||||
|
||||
@property
|
||||
def _robot_state_features(self)-> dict:
|
||||
if self._control_space == "joint":
|
||||
state_features = {f"{self.prefix}J{motor}.pos": float for motor in range(1, self._dof+1)}
|
||||
if self._jnt_obs_has_vel:
|
||||
state_features.update({f"{self.prefix}J{motor}.vel": float for motor in range(1, self._dof+1)})
|
||||
if self._gripper_type > GripperType.NoGripper:
|
||||
state_features.update({f"{self.prefix}gripper.pos": float})
|
||||
elif self._control_space == "cartesian":
|
||||
state_features = {f"{self.prefix}{key}": float for key in CARTESIAN_OBS_KEYS}
|
||||
if self._gripper_type > GripperType.NoGripper:
|
||||
state_features.update({f"{self.prefix}gripper.pos": float})
|
||||
else:
|
||||
raise ValueError(f"Please check the given control space of uf_robot! got {self._control_space}")
|
||||
return state_features
|
||||
|
||||
@property
|
||||
# CHECK!! channel first or last?
|
||||
def _cam_features(self) -> dict:
|
||||
cam_ft = {}
|
||||
for cam_key, cam in self.cameras.items():
|
||||
cam_ft[f"{self.prefix}{cam_key}"] = (cam.height, cam.width, 3)
|
||||
return cam_ft
|
||||
|
||||
@property
|
||||
def observation_features(self) -> dict[str, type | tuple]:
|
||||
return {**self._robot_state_features, **self._cam_features}
|
||||
|
||||
@property
|
||||
def action_features(self)-> dict:
|
||||
if self._control_space == "joint":
|
||||
action_ft = {f"{self.prefix}J{motor}.pos": float for motor in range(1, self._dof+1)}
|
||||
elif self._control_space == "cartesian":
|
||||
action_ft = {f"{self.prefix}{key}": float for key in CARTESIAN_ACTION_KEYS}
|
||||
else:
|
||||
raise ValueError(f"Please check the given control space of uf_robot! got {self._control_space}")
|
||||
# Consider adding velocity configuration ??
|
||||
if self._gripper_type > GripperType.NoGripper:
|
||||
action_ft.update({f"{self.prefix}gripper.pos": float})
|
||||
return action_ft
|
||||
|
||||
def connect(self, calibrate: bool = True) -> None:
|
||||
self.real_arm = XArmAPI(self.config.robot_ip)
|
||||
time.sleep(0.2)
|
||||
self._is_connected = self.real_arm.connected
|
||||
if not self._is_connected:
|
||||
print(f"UF Robot connection Failed, please check the hardware availability at ip: {self.config.robot_ip}")
|
||||
raise ConnectionError()
|
||||
|
||||
if not self._dof == self.real_arm.axis:
|
||||
print(f"[ERROR: ] Real Robot DOF({self.real_arm.axis}) does not match configuration ({self._dof})!")
|
||||
self._is_connected = False
|
||||
raise ConnectionError()
|
||||
|
||||
for cam in self.cameras.values():
|
||||
cam.connect()
|
||||
self._is_connected = self._is_connected and cam.is_connected
|
||||
|
||||
if not self._is_connected:
|
||||
print("Could not connect to the cameras, check that all cameras are plugged-in.")
|
||||
raise ConnectionError()
|
||||
|
||||
# if self._gripper_type == GripperType.PikaGripper:
|
||||
# if not self.pika_gripper.connect():
|
||||
# print('Could not connect to pika gripper.')
|
||||
# raise ConnectionError()
|
||||
|
||||
self.configure()
|
||||
if calibrate:
|
||||
self.calibrate()
|
||||
|
||||
self._is_connected = True
|
||||
|
||||
def configure(self) -> None:
|
||||
self.real_arm.motion_enable()
|
||||
self.real_arm.clean_error()
|
||||
self.real_arm.set_mode(0) # set to idle mode
|
||||
self.real_arm.set_state(0) # set to start state
|
||||
time.sleep(0.5)
|
||||
if self._start_tcp_pose is None:
|
||||
self.real_arm.set_servo_angle(angle=self._start_joints, is_radian=True, wait=True)
|
||||
else:
|
||||
self.real_arm.set_servo_angle(angle=self._start_joints, is_radian=True, wait=True)
|
||||
self.real_arm.set_position(*self._start_tcp_pose, speed=100, is_radian=True, wait=True)
|
||||
_, self._start_joints = self.real_arm.get_servo_angle(is_radian=True)
|
||||
self._start_tcp_pose = None
|
||||
|
||||
if self._control_space == "joint":
|
||||
self.real_arm.set_mode(6)
|
||||
elif self._control_space == "cartesian":
|
||||
self.real_arm.set_mode(7)
|
||||
else:
|
||||
raise ValueError(f"Please check the given control space of uf_robot! got {self._control_space}")
|
||||
|
||||
self.real_arm.set_state(0)
|
||||
|
||||
_, err_warn = self.real_arm.get_err_warn_code()
|
||||
if err_warn[0] != 0:
|
||||
raise RuntimeError(f"Failed to set correct state to UF robot! Controller Error code: {err_warn[0]} !")
|
||||
|
||||
if self._gripper_type > GripperType.NoGripper:
|
||||
self.real_arm._arm._baud_checkset = True
|
||||
if self._gripper_type == GripperType.xArmGripper:
|
||||
self.real_arm.set_gripper_enable(True)
|
||||
self.real_arm.set_gripper_mode(0)
|
||||
self.real_arm.set_gripper_speed(self._gripper_param.speed)
|
||||
self.real_arm.set_gripper_position(self._gripper_param.open_pos)
|
||||
elif self._gripper_type == GripperType.xArmGripperG2:
|
||||
self.real_arm.set_gripper_enable(True)
|
||||
self.real_arm.set_gripper_mode(0)
|
||||
self.real_arm.set_gripper_g2_position(self._gripper_param.open_pos)
|
||||
elif self._gripper_type == GripperType.BioGripperG2:
|
||||
_, mode = self.real_arm.get_bio_gripper_control_mode()
|
||||
if mode != 1:
|
||||
self.real_arm.set_bio_gripper_control_mode(1)
|
||||
self.real_arm.set_bio_gripper_enable(True)
|
||||
self.real_arm.open_bio_gripper()
|
||||
elif self._gripper_type == GripperType.PikaGripper:
|
||||
self.pika_gripper.enable()
|
||||
time.sleep(0.5)
|
||||
self.pika_gripper.set_gripper_distance(self._gripper_param.open_pos)
|
||||
elif self._gripper_type == GripperType.RobotiqGripper:
|
||||
self.real_arm.robotiq_reset()
|
||||
self.real_arm.robotiq_set_activate(wait=True)
|
||||
self.real_arm.robotiq_set_position(self._gripper_param.open_pos, wait=True)
|
||||
self._gripper_param.grippos = self._gripper_param.open_pos
|
||||
self._gripper_param.gripper_norm = self._gripper_param.open_pos
|
||||
self.real_arm._arm._baud_checkset = False
|
||||
_, err_warn = self.real_arm.get_err_warn_code()
|
||||
if err_warn[0] != 0:
|
||||
raise RuntimeError(f"Failed to set correct state to Gripper! Controller Error code: {err_warn[0]} !")
|
||||
|
||||
if self._use_rt_report and not self._rt_report_normal:
|
||||
self.start()
|
||||
time.sleep(0.2)
|
||||
|
||||
def calibrate(self) -> None:
|
||||
self._is_calibrated = True
|
||||
pass # CHECK! currently No-op
|
||||
|
||||
def get_observation(self) -> dict[str, np.ndarray]:
|
||||
obs_dict = {}
|
||||
|
||||
# Read Stretch state
|
||||
before_read_t = time.perf_counter()
|
||||
if self._control_space == "joint":
|
||||
code, states = self.real_arm.get_joint_states(is_radian=True, num=3)
|
||||
pos_list = states[0].copy()
|
||||
obs_dict = {f"{self.prefix}J{k+1}.pos": pos_list[k] for k in range(self._dof)}
|
||||
if self._jnt_obs_has_vel:
|
||||
vel_list = states[1].copy()
|
||||
obs_dict.update({f"{self.prefix}J{k+1}.vel": vel_list[k] for k in range(self._dof)})
|
||||
elif self._control_space == "cartesian":
|
||||
if not self._rt_report_normal:
|
||||
raise ConnectionError("RT Report for target robot NOT READY! ")
|
||||
|
||||
with self._update_lock:
|
||||
pos_list = self.rt_actual_tcp_pose.copy()
|
||||
vel_list = self.rt_actual_tcp_speed.copy()
|
||||
# pos_cmd_list = self.rt_cmd_tcp_pose.copy()
|
||||
# vel_cmd_list = self.rt_cmd_tcp_vel.copy()
|
||||
# jpos_fbk_list = self.rt_actual_joint_pos.copy()
|
||||
# jvel_fbk_list = self.rt_actual_joint_speed.copy()
|
||||
|
||||
obs_dict = {f"{self.prefix}pose.x": pos_list[0], f"{self.prefix}pose.y": pos_list[1], f"{self.prefix}pose.z": pos_list[2], f"{self.prefix}pose.rx": pos_list[3], f"{self.prefix}pose.ry": pos_list[4], f"{self.prefix}pose.rz": pos_list[5]}
|
||||
if self._cart_obs_has_vel:
|
||||
obs_dict.update({f"{self.prefix}velo.x": vel_list[0], f"{self.prefix}velo.y": vel_list[1], f"{self.prefix}velo.z": vel_list[2], f"{self.prefix}velo.rx": vel_list[3], f"{self.prefix}velo.ry": vel_list[4], f"{self.prefix}velo.rz": vel_list[5]})
|
||||
else:
|
||||
ValueError(f"Please check the given control space of uf_robot! got {self._control_space}")
|
||||
|
||||
if self._gripper_type > GripperType.NoGripper:
|
||||
if self._gripper_type == GripperType.xArmGripper:
|
||||
code, grippos = self.real_arm.get_gripper_position()
|
||||
grippos_norm = self._gripper_param.get_gripper_norm(grippos)
|
||||
elif self._gripper_type == GripperType.xArmGripperG2:
|
||||
code, grippos = self.real_arm.get_gripper_g2_position()
|
||||
grippos_norm = self._gripper_param.get_gripper_norm(grippos)
|
||||
elif self._gripper_type == GripperType.BioGripperG2:
|
||||
code, grippos = self.real_arm.get_bio_gripper_g2_position()
|
||||
grippos_norm = self._gripper_param.get_gripper_norm(grippos)
|
||||
elif self._gripper_type == GripperType.PikaGripper:
|
||||
grippos = self.pika_gripper.get_gripper_distance()
|
||||
grippos_norm = self._gripper_param.get_gripper_norm(grippos)
|
||||
elif self._gripper_type == GripperType.RobotiqGripper:
|
||||
self.real_arm.robotiq_get_status(number_of_registers=3)
|
||||
grippos = self.real_arm.robotiq_status['gPO'] # 0..255
|
||||
grippos_norm = self._gripper_param.get_gripper_norm(grippos) # 0=open, 1=closed
|
||||
self.logs["read_pos_dt_s"] = time.perf_counter() - before_read_t
|
||||
obs_dict[f"{self.prefix}gripper.pos"] = grippos_norm
|
||||
|
||||
# Capture images from cameras
|
||||
for cam_key, cam in self.cameras.items():
|
||||
before_camread_t = time.perf_counter()
|
||||
obs_dict[f"{self.prefix}{cam_key}"] = cam.async_read()
|
||||
self.logs[f"async_read_camera_{cam_key}_dt_s"] = time.perf_counter() - before_camread_t
|
||||
|
||||
return obs_dict
|
||||
|
||||
def send_action(self, action: dict) -> np.ndarray:
|
||||
if not self._is_connected:
|
||||
raise ConnectionError()
|
||||
if self.real_arm.error_code != 0:
|
||||
return action
|
||||
if self.config.no_action:
|
||||
return action
|
||||
|
||||
before_write_t = time.perf_counter()
|
||||
if self._control_space == "joint":
|
||||
# first sync with gello or other control device SLOWLY!
|
||||
jnt_spd = INIT_SYNC_JOINT_VELOCITY_RAD if self._cmd_cnt < 20 else self._max_joint_velocity
|
||||
wait_ = True if self._cmd_cnt == 0 else False
|
||||
|
||||
cmd_list = [0]*(self._dof)
|
||||
for i in range(self._dof):
|
||||
cmd_list[i] = action[f"{self.prefix}J{i+1}.pos"]
|
||||
|
||||
# TODO: make mode 6 compatible with wait=True
|
||||
if wait_== False and self.real_arm.mode != 6:
|
||||
self.real_arm.set_mode(6)
|
||||
self.real_arm.set_state(0)
|
||||
time.sleep(0.1)
|
||||
elif wait_ and self.real_arm.mode != 0:
|
||||
self.real_arm.set_mode(0)
|
||||
self.real_arm.set_state(0)
|
||||
time.sleep(0.1)
|
||||
|
||||
self.real_arm.set_servo_angle(angle=cmd_list[:self._dof], speed=jnt_spd, is_radian=True, wait=wait_)
|
||||
elif self._control_space == "cartesian": # unit: mm?
|
||||
lin_spd = self._max_linear_velocity
|
||||
|
||||
if not self._rt_report_normal:
|
||||
raise ConnectionError("RT Report for target robot NOT READY! ")
|
||||
cmd_list = [action[f"{self.prefix}pose.x"], action[f"{self.prefix}pose.y"], action[f"{self.prefix}pose.z"], action[f"{self.prefix}pose.rx"], action[f"{self.prefix}pose.ry"], action[f"{self.prefix}pose.rz"]]
|
||||
self.real_arm.set_position_aa(axis_angle_pose=cmd_list, speed=lin_spd, is_radian=True, wait=False)
|
||||
# self.real_arm.set_position(*cmd_list, radius=0, speed=lin_spd, is_radian=True, wait=False)
|
||||
|
||||
if self._cmd_cnt < 99999:
|
||||
self._cmd_cnt += 1 # CHECK!! possibility of overflow?
|
||||
if self._gripper_type > GripperType.NoGripper:
|
||||
gripper_norm = action[f"{self.prefix}gripper.pos"]
|
||||
if self._gripper_type == GripperType.xArmGripper:
|
||||
grippos = self._gripper_param.get_grippos(gripper_norm)
|
||||
modbus_datas = [0x08, 0x10, 0x07, 0x00, 0x00, 0x02, 0x04]
|
||||
modbus_datas.extend(list(struct.pack('>i', grippos)))
|
||||
self.real_arm.getset_tgpio_modbus_data(modbus_datas)
|
||||
# self.real_arm.set_gripper_position(grippos, wait=False, wait_motion=False) # CHECK! the command unit
|
||||
elif self._gripper_type == GripperType.xArmGripperG2:
|
||||
grippos = self._gripper_param.get_grippos(gripper_norm)
|
||||
grippos = int((math.degrees(math.asin((grippos - 16) / 110)) + 8.33) * 18.28)
|
||||
modbus_datas = [0x08, 0x10, 0x0C, 0x00, 0x00, 0x05, 0x0A, 0x00, 0x01]
|
||||
modbus_datas.extend(list(struct.pack('>h', self._gripper_param.speed)))
|
||||
modbus_datas.extend(list(struct.pack('>h', self._gripper_param.force)))
|
||||
modbus_datas.extend(list(struct.pack('>i', grippos)))
|
||||
self.real_arm.getset_tgpio_modbus_data(modbus_datas)
|
||||
elif self._gripper_type == GripperType.BioGripperG2:
|
||||
grippos = self._gripper_param.get_grippos(gripper_norm)
|
||||
grippos = int(grippos * 3.7342 - 265.13)
|
||||
modbus_datas = [0x08, 0x10, 0x0C, 0x00, 0x00, 0x05, 0x0A, 0x00, 0x01]
|
||||
modbus_datas.extend(list(struct.pack('>h', self._gripper_param.speed)))
|
||||
modbus_datas.extend(list(struct.pack('>h', self._gripper_param.force)))
|
||||
modbus_datas.extend(list(struct.pack('>i', grippos)))
|
||||
self.real_arm.getset_tgpio_modbus_data(modbus_datas)
|
||||
elif self._gripper_type == GripperType.PikaGripper:
|
||||
grippos = self._gripper_param.get_grippos(gripper_norm)
|
||||
self.pika_gripper.set_gripper_distance(grippos)
|
||||
elif self._gripper_type == GripperType.RobotiqGripper:
|
||||
grippos = self._gripper_param.get_grippos(gripper_norm)
|
||||
modbus_datas = [0x09, 0x10, 0x03, 0xE8, 0x00, 0x03, 0x06, 0x09, 0x00, 0x00, grippos, self._gripper_param.speed, self._gripper_param.force]
|
||||
self.real_arm.getset_tgpio_modbus_data(modbus_datas)
|
||||
# self.real_arm.robotiq_set_position(
|
||||
# grippos, speed=self._gripper_param.speed, force=self._gripper_param.force,
|
||||
# wait=False, wait_motion=False,
|
||||
# )
|
||||
|
||||
self.logs["write_pos_dt_s"] = time.perf_counter() - before_write_t
|
||||
return action
|
||||
|
||||
def print_logs(self) -> None:
|
||||
pass
|
||||
|
||||
def disconnect(self) -> None:
|
||||
self.real_arm.set_state(4) # stop
|
||||
self.real_arm.set_mode(0)
|
||||
if self._use_rt_report:
|
||||
self.report_stop_event.set()
|
||||
self.join()
|
||||
self.real_arm.disconnect()
|
||||
# CHECK!! how about gripper?
|
||||
|
||||
for cam in self.cameras.values():
|
||||
cam.disconnect()
|
||||
|
||||
self._is_connected = False
|
||||
|
||||
def is_calibrated(self) -> bool:
|
||||
"""Whether the robot is currently calibrated or not. Should be always `True` if not applicable"""
|
||||
return self._is_calibrated
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Whether the robot is currently calibrated or not. Should be always `True` if not applicable"""
|
||||
return self._is_connected
|
||||
|
||||
def run(self):
|
||||
import socket
|
||||
|
||||
robot_port = 30000 # DO NOT CHANGE
|
||||
# create socket connection
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.setblocking(True)
|
||||
sock.settimeout(1)
|
||||
sock.connect((self.config.robot_ip, robot_port))
|
||||
|
||||
buffer = sock.recv(4)
|
||||
print(buffer)
|
||||
while len(buffer) < 4:
|
||||
buffer += sock.recv(4 - len(buffer))
|
||||
size = convert.bytes_to_u32(buffer[:4])
|
||||
print(f"UFACTORY Robot ({self.config.robot_ip}) RT Report Thread starts!! =======")
|
||||
while not self.report_stop_event.is_set():
|
||||
buffer += sock.recv(size - len(buffer))
|
||||
if len(buffer) < size:
|
||||
continue
|
||||
data = buffer[:size]
|
||||
buffer = buffer[size:]
|
||||
with self._update_lock:
|
||||
self.rt_actual_joint_pos = convert.bytes_to_fp32s(data[116:144], 7)
|
||||
self.rt_actual_joint_speed = convert.bytes_to_fp32s(data[144:172], 7)
|
||||
self.rt_cmd_tcp_pose = convert.bytes_to_fp32s(data[424:448], 6)
|
||||
self.rt_cmd_tcp_vel = convert.bytes_to_fp32s(data[448:472], 6)
|
||||
self.rt_actual_tcp_pose = convert.bytes_to_fp32s(data[472:496], 6)
|
||||
self.rt_actual_tcp_speed = convert.bytes_to_fp32s(data[496:520], 6)
|
||||
self._rt_report_normal = True
|
||||
|
||||
self._rt_report_normal = False
|
||||
print(f"UFACTORY Robot ({self.config.robot_ip}) RT Report Thread Exit!! =======")
|
||||
44
ufactory_lerobot/robots/uf_robot/uf_robot_config.py
Normal file
44
ufactory_lerobot/robots/uf_robot/uf_robot_config.py
Normal file
@ -0,0 +1,44 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Tuple
|
||||
import numpy as np
|
||||
from lerobot.cameras import CameraConfig
|
||||
from lerobot.cameras.realsense import RealSenseCameraConfig
|
||||
from lerobot.robots import RobotConfig
|
||||
|
||||
@RobotConfig.register_subclass("uf::robot")
|
||||
@dataclass
|
||||
class UFRobotConfig(RobotConfig):
|
||||
# cameras
|
||||
cameras: dict[str, CameraConfig] = field(
|
||||
default_factory=lambda: {
|
||||
"overhead": RealSenseCameraConfig(
|
||||
serial_number_or_name="Intel RealSense D435I",
|
||||
fps=30,
|
||||
width=640, # 1280
|
||||
height=480, # 720
|
||||
# rotation=90,
|
||||
),
|
||||
"tool": RealSenseCameraConfig(
|
||||
serial_number_or_name="Intel RealSense D435",
|
||||
fps=30,
|
||||
width=640, # 1280
|
||||
height=480, # 720
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
robot_ip: str = "192.168.1.127"
|
||||
robot_dof: int | None = None # Set it correctly if controlling in joint space!
|
||||
control_space: str = "joint"
|
||||
gripper_control: bool = True
|
||||
gripper_type: int = 1 # 1: xArm Gripper, 2: xArm Gripper G2, 10: Pika Gripper, 11: Robotiq 2F-85
|
||||
gripper_port: str = None # only used by pika gripper (gripper_type=10)
|
||||
gripper_speed: int = -1 # auto
|
||||
gripper_force: int = -1 # auto
|
||||
observe_joint_vel: bool = False # only effective in joint control mode
|
||||
start_joints: Tuple[float, ...] = (0, 0, 0, np.pi/2, 0, np.pi/2, 0)
|
||||
start_tcp_pose: Tuple[float, ...] = None # xyzrpy
|
||||
max_joint_velocity: int = 90 # °/s, only effective in joint control mode
|
||||
max_linear_velocity: int = 200 # mm/s, only effective in cartesian control mode
|
||||
rx_continuous: bool = False
|
||||
no_action: bool = False
|
||||
34
ufactory_lerobot/robots/utils.py
Normal file
34
ufactory_lerobot/robots/utils.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from lerobot.robots.utils import make_robot_from_config as lerobot_make_robot_from_config
|
||||
from lerobot.robots.config import RobotConfig
|
||||
from lerobot.robots.robot import Robot
|
||||
|
||||
|
||||
def make_robot_from_config(config: RobotConfig) -> Robot:
|
||||
if config.type == "uf::robot":
|
||||
from .uf_robot import UFRobot
|
||||
return UFRobot(config)
|
||||
elif config.type == "uf::multiple_robot":
|
||||
from .uf_robot import MultipleUFRobot
|
||||
return MultipleUFRobot(config)
|
||||
elif config.type == "uf::mock_robot":
|
||||
from .uf_mock_robot import UFMockRobot
|
||||
return UFMockRobot(config)
|
||||
elif config.type == "uf::multiple_mock_robot":
|
||||
from .uf_mock_robot import MultipleUFMockRobot
|
||||
return MultipleUFMockRobot(config)
|
||||
else:
|
||||
return lerobot_make_robot_from_config(config)
|
||||
443
ufactory_lerobot/scripts/uf_lerobot_eval.py
Normal file
443
ufactory_lerobot/scripts/uf_lerobot_eval.py
Normal file
@ -0,0 +1,443 @@
|
||||
import yaml
|
||||
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
|
||||
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
|
||||
from lerobot.policies.utils import make_robot_action
|
||||
from lerobot.policies.factory import make_policy, make_pre_post_processors
|
||||
from lerobot.datasets.lerobot_dataset import LeRobotDatasetMetadata
|
||||
from lerobot.utils.constants import OBS_STR
|
||||
from lerobot.processor import (
|
||||
make_default_processors,
|
||||
)
|
||||
from lerobot.robots import ( # noqa: F401
|
||||
Robot,
|
||||
RobotConfig,
|
||||
make_robot_from_config,
|
||||
)
|
||||
from lerobot.utils.control_utils import (
|
||||
is_headless,
|
||||
init_keyboard_listener,
|
||||
predict_action,
|
||||
)
|
||||
from lerobot.utils.import_utils import register_third_party_plugins
|
||||
from lerobot.utils.robot_utils import precise_sleep
|
||||
from lerobot.utils.utils import (
|
||||
get_safe_torch_device,
|
||||
init_logging,
|
||||
)
|
||||
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
|
||||
from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations
|
||||
|
||||
|
||||
def continuous_rotvec(new_rv, prev_rv):
|
||||
"""Keep rotvec in the same sign-hemisphere as prev to avoid ±π flips.
|
||||
When accumulated rotation crosses π, as_rotvec() can flip the axis sign
|
||||
(e.g. rx jumps 3.14 → -3.13), causing the robot to make a large motion.
|
||||
This re-maps the equivalent rotation to stay consistent with prev."""
|
||||
new_rv = np.asarray(new_rv, dtype=np.float64)
|
||||
prev_rv = np.asarray(prev_rv, dtype=np.float64)
|
||||
if np.dot(new_rv, prev_rv) < 0:
|
||||
angle = np.linalg.norm(new_rv)
|
||||
if angle > 1e-6:
|
||||
axis = new_rv / angle
|
||||
new_rv = -(2 * np.pi - angle) * axis
|
||||
return new_rv
|
||||
|
||||
def blend_poses(pose_a, pose_b, alpha):
|
||||
"""位姿混合: (1-alpha)*A + alpha*B, 旋转用 SO(3) 插值。
|
||||
先将 pose_b 的 rotvec 归一化到与 pose_a 同符号半球,避免 ±π 跳变破坏线性混合。"""
|
||||
blended_pos = (1 - alpha) * np.array(pose_a[:3]) + alpha * np.array(pose_b[:3])
|
||||
# 旋转用线性混合 rotvec (delta 很小时近似 SLERP)
|
||||
rot_b = continuous_rotvec(np.array(pose_b[3:6]), np.array(pose_a[3:6]))
|
||||
blended_rot = (1 - alpha) * np.array(pose_a[3:6]) + alpha * rot_b
|
||||
return np.concatenate([blended_pos, blended_rot]).tolist()
|
||||
|
||||
def compute_relative_axis_angle(rot_prev, rot_curr):
|
||||
"""
|
||||
计算两个轴角之间的相对旋转。
|
||||
逻辑: R_diff = R_prev.T @ R_curr
|
||||
返回: 相对轴角向量
|
||||
"""
|
||||
# 1. 转为矩阵
|
||||
R_prev = Transformations.rxryrz_to_matrix(rot_prev)
|
||||
R_curr = Transformations.rxryrz_to_matrix(rot_curr)
|
||||
|
||||
# 2. 计算相对旋转矩阵
|
||||
# R_delta 表示从 prev 坐标系到 curr 坐标系的旋转
|
||||
R_delta = R_prev.T @ R_curr
|
||||
|
||||
# 3. 转回轴角
|
||||
return Transformations.rotation_matrix_to_rxryrz(R_delta)
|
||||
|
||||
def compute_target_axis_angle(rot_prev, rot_delta):
|
||||
"""
|
||||
根据起始轴角和相对轴角计算目标轴角
|
||||
"""
|
||||
R_prev = Transformations.rxryrz_to_matrix(rot_prev)
|
||||
R_delta = Transformations.rxryrz_to_matrix(rot_delta)
|
||||
R_curr = R_prev @ R_delta
|
||||
# R_curr = R_prev.apply(R_delta)
|
||||
return Transformations.rotation_matrix_to_rxryrz(R_curr)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvalConfig:
|
||||
robot: RobotConfig
|
||||
dataset: DatasetRecordConfig
|
||||
# Whether to control the robot with a policy
|
||||
policy: PreTrainedConfig | None = None
|
||||
n_episodes: int = 50
|
||||
single_task: str | None = "pick_place"
|
||||
|
||||
def __post_init__(self):
|
||||
# HACK: We parse again the cli args here to get the pretrained path if there was one.
|
||||
policy_path = parser.get_path_arg("policy")
|
||||
if policy_path:
|
||||
cli_overrides = parser.get_cli_overrides("policy")
|
||||
policy_path = Path(policy_path).expanduser()
|
||||
self.policy = PreTrainedConfig.from_pretrained(policy_path, cli_overrides=cli_overrides)
|
||||
self.policy.pretrained_path = policy_path
|
||||
|
||||
if self.policy is None:
|
||||
raise ValueError("Choose a policy to control the robot")
|
||||
|
||||
@classmethod
|
||||
def __get_path_fields__(cls) -> list[str]:
|
||||
"""This enables the parser to load config from the policy using `--policy.path=local/dir`"""
|
||||
return ["policy"]
|
||||
|
||||
|
||||
def eval_loop(cfg: EvalConfig, relative=False):
|
||||
init_logging()
|
||||
logging.info(pformat(asdict(cfg)))
|
||||
|
||||
# print(cfg.robot)
|
||||
print(type(cfg.robot))
|
||||
if hasattr(cfg.robot, 'robots'):
|
||||
print(cfg.robot.robots.keys())
|
||||
exit(1)
|
||||
|
||||
robot = make_robot_from_config(cfg.robot)
|
||||
|
||||
teleop_action_processor, robot_action_processor, robot_observation_processor = make_default_processors()
|
||||
|
||||
try:
|
||||
dataset_metadata = LeRobotDatasetMetadata(repo_id=cfg.dataset.repo_id, root=cfg.dataset.root)
|
||||
dataset_features = dataset_metadata.features
|
||||
print("Loaded dataset metadata successfully.")
|
||||
except Exception:
|
||||
dataset_features = combine_feature_dicts(
|
||||
aggregate_pipeline_dataset_features(
|
||||
pipeline=teleop_action_processor,
|
||||
initial_features=create_initial_features(
|
||||
action=robot.action_features
|
||||
), # TODO(steven, pepijn): in future this should be come from teleop or policy
|
||||
use_videos=cfg.dataset.video,
|
||||
),
|
||||
aggregate_pipeline_dataset_features(
|
||||
pipeline=robot_observation_processor,
|
||||
initial_features=create_initial_features(observation=robot.observation_features),
|
||||
use_videos=cfg.dataset.video,
|
||||
),
|
||||
)
|
||||
# Create empty dataset or load existing saved episodes
|
||||
dataset = LeRobotDataset.create(
|
||||
cfg.dataset.repo_id,
|
||||
cfg.dataset.fps,
|
||||
root=cfg.dataset.root,
|
||||
robot_type=robot.name,
|
||||
features=dataset_features,
|
||||
use_videos=cfg.dataset.video,
|
||||
image_writer_processes=cfg.dataset.num_image_writer_processes,
|
||||
image_writer_threads=cfg.dataset.num_image_writer_threads_per_camera * len(robot.cameras),
|
||||
batch_encoding_size=cfg.dataset.video_encoding_batch_size,
|
||||
)
|
||||
dataset_metadata = dataset.meta
|
||||
print("Created new dataset metadata successfully.")
|
||||
|
||||
if cfg.dataset.fps != dataset_metadata.fps:
|
||||
raise ValueError(f"The dataset fps should be equal to requested fps ({dataset_metadata.fps} != {cfg.dataset.fps}).")
|
||||
|
||||
|
||||
policy = make_policy(cfg=cfg.policy, ds_meta=dataset_metadata)
|
||||
# policy.eval()
|
||||
|
||||
# The inference device is automatically set to match the detected hardware, overriding any previous device settings from training to ensure compatibility.
|
||||
preprocessor_overrides = {
|
||||
"device_processor": {"device": str(policy.config.device)},
|
||||
"rename_observations_processor": {"rename_map": cfg.dataset.rename_map},
|
||||
}
|
||||
|
||||
preprocessor, postprocessor = make_pre_post_processors(
|
||||
policy_cfg=cfg.policy,
|
||||
pretrained_path=cfg.policy.pretrained_path,
|
||||
preprocessor_overrides=preprocessor_overrides,
|
||||
dataset_stats=dataset_metadata.stats
|
||||
)
|
||||
|
||||
robot.connect()
|
||||
|
||||
events = {"reset": False, "exit": False}
|
||||
listener = None
|
||||
|
||||
if not is_headless():
|
||||
from pynput import keyboard
|
||||
|
||||
def on_press(key):
|
||||
try:
|
||||
if key == keyboard.Key.right:
|
||||
print("Right arrow key pressed. Resetting...")
|
||||
events["reset"] = True
|
||||
elif key == keyboard.Key.left:
|
||||
print("Left arrow key pressed. Resetting....")
|
||||
events["reset"] = True
|
||||
elif key == keyboard.Key.esc:
|
||||
print("Escape key pressed. Stopping ...")
|
||||
events["exit"] = True
|
||||
except Exception as e:
|
||||
print(f"Error handling key press: {e}")
|
||||
|
||||
listener, events = init_keyboard_listener(events=events, on_press=on_press)
|
||||
|
||||
device = get_safe_torch_device(policy.config.device, log=True)
|
||||
sleep_time_s = 1 / dataset_metadata.fps
|
||||
|
||||
# Gripper look-ahead: denormalization stats for peeking into the action queue
|
||||
_gripper_mean = dataset_metadata.stats['action']['mean'][-1].item()
|
||||
_gripper_std = dataset_metadata.stats['action']['std'][-1].item()
|
||||
_gripper_min = dataset_metadata.stats['action']['min'][-1].item()
|
||||
_gripper_max = dataset_metadata.stats['action']['max'][-1].item()
|
||||
is_act_policy = hasattr(policy.config, 'chunk_size')
|
||||
# ACT: lookahead 30 (~1s) 补偿 chunk 慢启动; DP: 队列仅 8 步,lookahead 4
|
||||
GRIPPER_LOOKAHEAD = 0 if is_act_policy else 4
|
||||
|
||||
# =====================================================
|
||||
# Chunk boundary smoothing: only damp large discontinuities at action chunk
|
||||
# boundaries (mean ~6mm jump) while preserving smooth within-chunk motion (~1mm).
|
||||
# When step-to-step cmd change exceeds SMOOTH_THRESHOLD, clamp it to that limit.
|
||||
# =====================================================
|
||||
SMOOTH_THRESHOLD = 0 # mm: smooth chunk boundary jumps while preserving trajectory
|
||||
SMOOTH_ROT_THRESHOLD = 0.05 # rad: max allowed rotation jump per step
|
||||
prev_smoothed_pose = None
|
||||
|
||||
print("\n********** Policy Eval Episode Loop Start **********")
|
||||
print(f'relative: {relative}')
|
||||
|
||||
rx_continuous = getattr(cfg.robot, 'rx_continuous', False)
|
||||
|
||||
# with torch.no_grad(), torch.autocast(device_type=device.type) if cfg.policy.use_amp else nullcontext():
|
||||
while True:
|
||||
robot.configure()
|
||||
policy.reset()
|
||||
preprocessor.reset()
|
||||
postprocessor.reset()
|
||||
|
||||
obs = robot.get_observation()
|
||||
|
||||
prev_robot_dict = {}
|
||||
prev_action_dict = {}
|
||||
|
||||
is_multiple_robot = False
|
||||
if hasattr(cfg.robot, 'robots'):
|
||||
keys = cfg.robot.robots.keys()
|
||||
is_multiple_robot = True
|
||||
else:
|
||||
keys = ['']
|
||||
for key in keys:
|
||||
prefix = f'.{key}' if key else ''
|
||||
is_tcp = f'{prefix}pose.x' in obs and f'{prefix}pose.y' in obs and f'{prefix}pose.z' in obs and f'{prefix}pose.rx' in obs and f'{prefix}pose.ry' in obs and f'{prefix}pose.rz' in obs
|
||||
if is_tcp:
|
||||
pose = [obs[f'{prefix}pose.x'], obs[f'{prefix}pose.y'], obs[f'{prefix}pose.z'], obs[f'{prefix}pose.rx'], obs[f'{prefix}pose.ry'], obs[f'{prefix}pose.rz']]
|
||||
else:
|
||||
pose = []
|
||||
prev_robot_dict[key] = {'type': 1 if is_tcp else 0, 'pose': np.array(pose)}
|
||||
prev_action_dict[key] = {'type': 1 if is_tcp else 0, 'pose': np.array(pose)}
|
||||
|
||||
while True:
|
||||
start_loop_t = time.perf_counter()
|
||||
|
||||
if events["reset"] or events["exit"]:
|
||||
events["reset"] = False
|
||||
print("\n********** Policy Eval Episode (Reset) **********")
|
||||
break
|
||||
|
||||
# Get robot observation
|
||||
obs = robot.get_observation()
|
||||
# if rx_continuous and not relative and 'pose.rx' in obs and obs['pose.rx'] < 0:
|
||||
# obs['pose.rx'] += 2 * math.pi
|
||||
curr_robot_dict = {}
|
||||
curr_action_dict = {}
|
||||
for key in keys:
|
||||
prefix = f'.{key}' if key else ''
|
||||
is_tcp = f'{prefix}pose.x' in obs and f'{prefix}pose.y' in obs and f'{prefix}pose.z' in obs and f'{prefix}pose.rx' in obs and f'{prefix}pose.ry' in obs and f'{prefix}pose.rz' in obs
|
||||
if not is_tcp or prev_robot_dict[key]['type'] != 1:
|
||||
curr_robot_dict[key] = {'type': 0, 'pose': []}
|
||||
continue
|
||||
if rx_continuous and not relative and f'{prefix}pose.rx' in obs and obs[f'{prefix}pose.rx'] < 0:
|
||||
obs[f'{prefix}pose.rx'] += 2 * np.pi
|
||||
prev_robot_pose = prev_robot_dict[key]['pose']
|
||||
curr_robot_pose = np.array([obs[f'{prefix}pose.x'], obs[f'{prefix}pose.y'], obs[f'{prefix}pose.z'], obs[f'{prefix}pose.rx'], obs[f'{prefix}pose.ry'], obs[f'{prefix}pose.rz']])
|
||||
curr_rot_normalized = continuous_rotvec(curr_robot_pose[3:6], prev_robot_pose[3:6])
|
||||
curr_robot_pose[3] = float(curr_rot_normalized[0])
|
||||
curr_robot_pose[4] = float(curr_rot_normalized[1])
|
||||
curr_robot_pose[5] = float(curr_rot_normalized[2])
|
||||
curr_robot_dict[key] = {'type': 1, 'pose': curr_robot_pose}
|
||||
|
||||
if relative:
|
||||
delta = compute_relative_axis_angle(prev_robot_pose[3:6], curr_robot_pose[3:6])
|
||||
obs[f'{prefix}pose.x'] = curr_robot_pose[0] - prev_robot_pose[0]
|
||||
obs[f'{prefix}pose.y'] = curr_robot_pose[1] - prev_robot_pose[1]
|
||||
obs[f'{prefix}pose.z'] = curr_robot_pose[2] - prev_robot_pose[2]
|
||||
obs[f'{prefix}pose.rx'] = delta[0]
|
||||
obs[f'{prefix}pose.ry'] = delta[1]
|
||||
obs[f'{prefix}pose.rz'] = delta[2]
|
||||
prev_robot_dict[key]['pose'] = curr_robot_pose
|
||||
|
||||
# Applies a pipeline to the raw robot observation, default is IdentityProcessor
|
||||
obs_processed = robot_observation_processor(obs)
|
||||
|
||||
observation_frame = build_dataset_frame(dataset_features, obs_processed, prefix=OBS_STR)
|
||||
|
||||
action_values = predict_action(
|
||||
observation=observation_frame,
|
||||
policy=policy,
|
||||
device=device,
|
||||
preprocessor=preprocessor,
|
||||
postprocessor=postprocessor,
|
||||
use_amp=policy.config.use_amp,
|
||||
task=cfg.single_task,
|
||||
robot_type=robot.robot_type,
|
||||
)
|
||||
act_processed_policy = make_robot_action(action_values, dataset_features)
|
||||
robot_action_to_send = robot_action_processor((act_processed_policy, obs))
|
||||
|
||||
for key in keys:
|
||||
prefix = f'.{key}' if key else ''
|
||||
if not curr_robot_dict[key]['type'] != 1 or prev_action_dict[key]['type'] != 1:
|
||||
continue
|
||||
if relative:
|
||||
rot_delta = np.array([robot_action_to_send[f'{prefix}pose.rx'], robot_action_to_send[f'{prefix}pose.ry'], robot_action_to_send[f'{prefix}pose.rz']])
|
||||
prev_action_pose = prev_action_dict[key]['pose']
|
||||
rot_curr = compute_target_axis_angle(prev_action_pose[3:6], rot_delta)
|
||||
robot_action_to_send[f'{prefix}pose.x'] = prev_action_pose[0] + robot_action_to_send[f'{prefix}pose.x']
|
||||
robot_action_to_send[f'{prefix}pose.y'] = prev_action_pose[1] + robot_action_to_send[f'{prefix}pose.y']
|
||||
robot_action_to_send[f'{prefix}pose.z'] = prev_action_pose[2] + robot_action_to_send[f'{prefix}pose.z']
|
||||
robot_action_to_send[f'{prefix}pose.rx'] = rot_curr[0]
|
||||
robot_action_to_send[f'{prefix}pose.ry'] = rot_curr[1]
|
||||
robot_action_to_send[f'{prefix}pose.rz'] = rot_curr[2]
|
||||
elif rx_continuous and f'{prefix}pose.rx' in robot_action_to_send and robot_action_to_send[f'{prefix}pose.rx'] > math.pi:
|
||||
robot_action_to_send[f'{prefix}pose.rx'] -= 2 * np.pi
|
||||
|
||||
# robot_action_to_send[f'{prefix}pose.z'] = max(robot_action_to_send[f'{prefix}pose.z'], 199)
|
||||
|
||||
# Rate-limited smoothing: cap position velocity to reduce chunk boundary jerks
|
||||
# Uses vector-norm clamping to preserve motion direction
|
||||
if SMOOTH_THRESHOLD > 0:
|
||||
pos_keys = [f'{prefix}pose.x', f'{prefix}pose.y', f'{prefix}pose.z']
|
||||
rot_keys = [f'{prefix}pose.rx', f'{prefix}pose.ry', f'{prefix}pose.rz']
|
||||
if prev_smoothed_pose is None:
|
||||
prev_smoothed_pose = {k: robot_action_to_send[k] for k in pos_keys + rot_keys}
|
||||
else:
|
||||
# Vector-norm clamp on position (preserves direction)
|
||||
delta_pos = np.array([robot_action_to_send[k] - prev_smoothed_pose[k] for k in pos_keys])
|
||||
norm = np.linalg.norm(delta_pos)
|
||||
if norm > SMOOTH_THRESHOLD:
|
||||
delta_pos = delta_pos * (SMOOTH_THRESHOLD / norm)
|
||||
for i, k in enumerate(pos_keys):
|
||||
prev_smoothed_pose[k] = prev_smoothed_pose[k] + delta_pos[i]
|
||||
robot_action_to_send[k] = prev_smoothed_pose[k]
|
||||
# Per-axis clamp on rotation
|
||||
for k in rot_keys:
|
||||
delta = robot_action_to_send[k] - prev_smoothed_pose[k]
|
||||
if abs(delta) > SMOOTH_ROT_THRESHOLD:
|
||||
delta = SMOOTH_ROT_THRESHOLD * (1 if delta > 0 else -1)
|
||||
prev_smoothed_pose[k] = prev_smoothed_pose[k] + delta
|
||||
robot_action_to_send[k] = prev_smoothed_pose[k]
|
||||
|
||||
if relative:
|
||||
curr_action_pose = np.array([
|
||||
robot_action_to_send[f'{prefix}pose.x'], robot_action_to_send[f'{prefix}pose.y'], robot_action_to_send[f'{prefix}pose.z'],
|
||||
robot_action_to_send[f'{prefix}pose.rx'], robot_action_to_send[f'{prefix}pose.ry'], robot_action_to_send[f'{prefix}pose.rz']
|
||||
])
|
||||
# 相对增量模式: 漂移修正,将指令位姿温和拉回实际位姿
|
||||
# prev_action_pose = blend_poses(curr_action_pose, curr_robot_pose, 0.05)
|
||||
prev_action_dict[key]['pose'] = curr_action_pose
|
||||
|
||||
# # Gripper look-ahead: peek ahead in the action queue to compensate
|
||||
# # for the slow ramp in the ACT chunk (eliminates 1-2s gripper delay)
|
||||
# gripper_raw = robot_action_to_send.get('left.gripper.pos', 0)
|
||||
# if hasattr(policy, '_action_queue') and len(policy._action_queue) > 0:
|
||||
# # ACT: 队列为 deque of tensors, 归一化方式 MEAN_STD
|
||||
# lookahead_idx = min(GRIPPER_LOOKAHEAD, len(policy._action_queue) - 1)
|
||||
# future_gripper_norm = policy._action_queue[lookahead_idx][0, -1].item()
|
||||
# gripper_raw = future_gripper_norm * _gripper_std + _gripper_mean
|
||||
# elif hasattr(policy, '_queues') and 'action' in policy._queues and len(policy._queues['action']) > 0:
|
||||
# # DP: 队列结构不同, 归一化方式 MIN_MAX → [-1,1] → [min,max]
|
||||
# lookahead_idx = min(GRIPPER_LOOKAHEAD, len(policy._queues['action']) - 1)
|
||||
# future_gripper_norm = policy._queues['action'][lookahead_idx][0, -1].item()
|
||||
# gripper_raw = (future_gripper_norm + 1) / 2 * (_gripper_max - _gripper_min) + _gripper_min
|
||||
# robot_action_to_send['left.gripper.pos'] = 1.0 if gripper_raw > 0.4 else 0.0
|
||||
|
||||
# gripper_raw = robot_action_to_send.get('right.gripper.pos', 0)
|
||||
# if hasattr(policy, '_action_queue') and len(policy._action_queue) > 0:
|
||||
# # ACT: 队列为 deque of tensors, 归一化方式 MEAN_STD
|
||||
# lookahead_idx = min(GRIPPER_LOOKAHEAD, len(policy._action_queue) - 1)
|
||||
# future_gripper_norm = policy._action_queue[lookahead_idx][0, -1].item()
|
||||
# gripper_raw = future_gripper_norm * _gripper_std + _gripper_mean
|
||||
# elif hasattr(policy, '_queues') and 'action' in policy._queues and len(policy._queues['action']) > 0:
|
||||
# # DP: 队列结构不同, 归一化方式 MIN_MAX → [-1,1] → [min,max]
|
||||
# lookahead_idx = min(GRIPPER_LOOKAHEAD, len(policy._queues['action']) - 1)
|
||||
# future_gripper_norm = policy._queues['action'][lookahead_idx][0, -1].item()
|
||||
# gripper_raw = (future_gripper_norm + 1) / 2 * (_gripper_max - _gripper_min) + _gripper_min
|
||||
# robot_action_to_send['right.gripper.pos'] = 1.0 if gripper_raw > 0.4 else 0.0
|
||||
|
||||
robot.send_action(robot_action_to_send)
|
||||
|
||||
dt_s = time.perf_counter() - start_loop_t
|
||||
precise_sleep(sleep_time_s - dt_s)
|
||||
|
||||
if events["exit"]:
|
||||
break
|
||||
|
||||
print("\n********** Policy Eval Loop Exit **********")
|
||||
if not is_headless() and listener is not None:
|
||||
listener.stop()
|
||||
|
||||
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')
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
364
ufactory_lerobot/scripts/uf_lerobot_record.py
Normal file
364
ufactory_lerobot/scripts/uf_lerobot_record.py
Normal file
@ -0,0 +1,364 @@
|
||||
import yaml
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import ufactory_lerobot # 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
|
||||
|
||||
|
||||
@safe_stop_image_writer
|
||||
def record_loop(
|
||||
robot: Robot,
|
||||
events: dict,
|
||||
fps: int,
|
||||
teleop_action_processor: RobotProcessorPipeline[
|
||||
tuple[RobotAction, RobotObservation], RobotAction
|
||||
], # runs after teleop
|
||||
robot_action_processor: RobotProcessorPipeline[
|
||||
tuple[RobotAction, RobotObservation], RobotAction
|
||||
], # runs before robot
|
||||
robot_observation_processor: RobotProcessorPipeline[
|
||||
RobotObservation, RobotObservation
|
||||
], # runs after robot
|
||||
dataset: LeRobotDataset | None = None,
|
||||
teleop: Teleoperator | list[Teleoperator] | None = None,
|
||||
policy: PreTrainedPolicy | None = None,
|
||||
preprocessor: PolicyProcessorPipeline[dict[str, Any], dict[str, Any]] | None = None,
|
||||
postprocessor: PolicyProcessorPipeline[PolicyAction, PolicyAction] | None = None,
|
||||
control_time_s: int | None = None,
|
||||
single_task: str | None = None,
|
||||
display_data: bool = False,
|
||||
display_compressed_images: bool = False,
|
||||
frame_callback: callable = None,
|
||||
):
|
||||
if dataset is not None and dataset.fps != fps:
|
||||
raise ValueError(f"The dataset fps should be equal to requested fps ({dataset.fps} != {fps}).")
|
||||
|
||||
teleop_arm = teleop_keyboard = None
|
||||
if isinstance(teleop, list):
|
||||
teleop_keyboard = next((t for t in teleop if isinstance(t, KeyboardTeleop)), None)
|
||||
teleop_arm = next(
|
||||
(
|
||||
t
|
||||
for t in teleop
|
||||
if isinstance(
|
||||
t,
|
||||
(
|
||||
so_leader.SO100Leader
|
||||
| so_leader.SO101Leader
|
||||
| koch_leader.KochLeader
|
||||
| omx_leader.OmxLeader
|
||||
),
|
||||
)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not (teleop_arm and teleop_keyboard and len(teleop) == 2 and robot.name == "lekiwi_client"):
|
||||
raise ValueError(
|
||||
"For multi-teleop, the list must contain exactly one KeyboardTeleop and one arm teleoperator. Currently only supported for LeKiwi robot."
|
||||
)
|
||||
|
||||
# Reset policy and processor if they are provided
|
||||
if policy is not None and preprocessor is not None and postprocessor is not None:
|
||||
policy.reset()
|
||||
preprocessor.reset()
|
||||
postprocessor.reset()
|
||||
|
||||
last_robot_cmd = robot.get_observation()
|
||||
# only positional cmd for now: Remove velo from observation for cmd if needed!
|
||||
last_robot_cmd = { k: v for k,v in last_robot_cmd.items() if not "vel" in k }
|
||||
|
||||
timestamp = 0
|
||||
start_episode_t = time.perf_counter()
|
||||
while timestamp < control_time_s:
|
||||
start_loop_t = time.perf_counter()
|
||||
|
||||
if events["exit_early"]:
|
||||
events["exit_early"] = False
|
||||
break
|
||||
|
||||
# Get robot observation
|
||||
obs = robot.get_observation()
|
||||
|
||||
# Applies a pipeline to the raw robot observation, default is IdentityProcessor
|
||||
obs_processed = robot_observation_processor(obs)
|
||||
|
||||
if policy is not None or dataset is not None:
|
||||
observation_frame = build_dataset_frame(dataset.features, obs_processed, prefix=OBS_STR)
|
||||
|
||||
# Get action from either policy or teleop
|
||||
if policy is not None and preprocessor is not None and postprocessor is not None:
|
||||
action_values = predict_action(
|
||||
observation=observation_frame,
|
||||
policy=policy,
|
||||
device=get_safe_torch_device(policy.config.device),
|
||||
preprocessor=preprocessor,
|
||||
postprocessor=postprocessor,
|
||||
use_amp=policy.config.use_amp,
|
||||
task=single_task,
|
||||
robot_type=robot.robot_type,
|
||||
)
|
||||
|
||||
act_processed_policy: RobotAction = make_robot_action(action_values, dataset.features)
|
||||
|
||||
elif policy is None and isinstance(teleop, Teleoperator):
|
||||
act = teleop.get_action()
|
||||
|
||||
# (space mouse) from delta Cartesian cmd to absolute command
|
||||
if "pose.dx" in act:
|
||||
last_robot_cmd.update({"pose.x": last_robot_cmd["pose.x"] + act["pose.dx"], "pose.y": last_robot_cmd["pose.y"] + act["pose.dy"], "pose.z": last_robot_cmd["pose.z"] + act["pose.dz"]})
|
||||
act = last_robot_cmd.copy() # watch out this is shallow copy, not for nested dict
|
||||
|
||||
# Applies a pipeline to the raw teleop action, default is IdentityProcessor
|
||||
act_processed_teleop = teleop_action_processor((act, obs))
|
||||
|
||||
elif policy is None and isinstance(teleop, list):
|
||||
arm_action = teleop_arm.get_action()
|
||||
arm_action = {f"arm_{k}": v for k, v in arm_action.items()}
|
||||
keyboard_action = teleop_keyboard.get_action()
|
||||
base_action = robot._from_keyboard_to_base_action(keyboard_action)
|
||||
act = {**arm_action, **base_action} if len(base_action) > 0 else arm_action
|
||||
act_processed_teleop = teleop_action_processor((act, obs))
|
||||
else:
|
||||
logging.info(
|
||||
"No policy or teleoperator provided, skipping action generation."
|
||||
"This is likely to happen when resetting the environment without a teleop device."
|
||||
"The robot won't be at its rest position at the start of the next episode."
|
||||
)
|
||||
continue
|
||||
|
||||
# Applies a pipeline to the action, default is IdentityProcessor
|
||||
if policy is not None and act_processed_policy is not None:
|
||||
action_values = act_processed_policy
|
||||
robot_action_to_send = robot_action_processor((act_processed_policy, obs))
|
||||
else:
|
||||
action_values = act_processed_teleop
|
||||
robot_action_to_send = robot_action_processor((act_processed_teleop, obs))
|
||||
|
||||
# Send action to robot
|
||||
# Action can eventually be clipped using `max_relative_target`,
|
||||
# so action actually sent is saved in the dataset. action = postprocessor.process(action)
|
||||
# TODO(steven, pepijn, adil): we should use a pipeline step to clip the action, so the sent action is the action that we input to the robot.
|
||||
_sent_action = robot.send_action(robot_action_to_send)
|
||||
|
||||
# Write to dataset
|
||||
if dataset is not None:
|
||||
action_frame = build_dataset_frame(dataset.features, action_values, prefix=ACTION)
|
||||
frame = {**observation_frame, **action_frame, "task": single_task}
|
||||
if frame_callback is not None:
|
||||
frame = frame_callback(frame)
|
||||
dataset.add_frame(frame)
|
||||
|
||||
if display_data:
|
||||
log_rerun_data(
|
||||
observation=obs_processed, action=action_values, compress_images=display_compressed_images
|
||||
)
|
||||
|
||||
dt_s = time.perf_counter() - start_loop_t
|
||||
precise_sleep(max(1 / fps - dt_s, 0.0))
|
||||
|
||||
timestamp = time.perf_counter() - start_episode_t
|
||||
|
||||
|
||||
@parser.wrap()
|
||||
def record(cfg: RecordConfig) -> 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_action_processor, robot_action_processor, robot_observation_processor = make_default_processors()
|
||||
|
||||
dataset_features = combine_feature_dicts(
|
||||
aggregate_pipeline_dataset_features(
|
||||
pipeline=teleop_action_processor,
|
||||
initial_features=create_initial_features(
|
||||
action=robot.action_features
|
||||
), # TODO(steven, pepijn): in future this should be come from teleop or policy
|
||||
use_videos=cfg.dataset.video,
|
||||
),
|
||||
aggregate_pipeline_dataset_features(
|
||||
pipeline=robot_observation_processor,
|
||||
initial_features=create_initial_features(observation=robot.observation_features),
|
||||
use_videos=cfg.dataset.video,
|
||||
),
|
||||
)
|
||||
|
||||
if cfg.resume:
|
||||
dataset = LeRobotDataset(
|
||||
cfg.dataset.repo_id,
|
||||
root=cfg.dataset.root,
|
||||
batch_encoding_size=cfg.dataset.video_encoding_batch_size,
|
||||
)
|
||||
|
||||
if hasattr(robot, "cameras") and len(robot.cameras) > 0:
|
||||
dataset.start_image_writer(
|
||||
num_processes=cfg.dataset.num_image_writer_processes,
|
||||
num_threads=cfg.dataset.num_image_writer_threads_per_camera * len(robot.cameras),
|
||||
)
|
||||
sanity_check_dataset_robot_compatibility(dataset, robot, cfg.dataset.fps, dataset_features)
|
||||
else:
|
||||
# Create empty dataset or load existing saved episodes
|
||||
sanity_check_dataset_name(cfg.dataset.repo_id, cfg.policy)
|
||||
dataset = LeRobotDataset.create(
|
||||
cfg.dataset.repo_id,
|
||||
cfg.dataset.fps,
|
||||
root=cfg.dataset.root,
|
||||
robot_type=robot.name,
|
||||
features=dataset_features,
|
||||
use_videos=cfg.dataset.video,
|
||||
image_writer_processes=cfg.dataset.num_image_writer_processes,
|
||||
image_writer_threads=cfg.dataset.num_image_writer_threads_per_camera * len(robot.cameras),
|
||||
batch_encoding_size=cfg.dataset.video_encoding_batch_size,
|
||||
)
|
||||
|
||||
# Load pretrained policy
|
||||
policy = None if cfg.policy is None else make_policy(cfg.policy, ds_meta=dataset.meta)
|
||||
preprocessor = None
|
||||
postprocessor = None
|
||||
if cfg.policy is not None:
|
||||
preprocessor, postprocessor = make_pre_post_processors(
|
||||
policy_cfg=cfg.policy,
|
||||
pretrained_path=cfg.policy.pretrained_path,
|
||||
dataset_stats=rename_stats(dataset.meta.stats, cfg.dataset.rename_map),
|
||||
preprocessor_overrides={
|
||||
"device_processor": {"device": cfg.policy.device},
|
||||
"rename_observations_processor": {"rename_map": cfg.dataset.rename_map},
|
||||
},
|
||||
)
|
||||
|
||||
robot.connect()
|
||||
if teleop is not None:
|
||||
teleop.connect()
|
||||
|
||||
listener, events = init_keyboard_listener()
|
||||
|
||||
print("\n********** Episode Record Loop Start **********")
|
||||
|
||||
if isinstance(teleop, UFBaseTeleop):
|
||||
# if getattr(cfg.robot, 'rx_continuous', False):
|
||||
# def frame_callback(frame):
|
||||
# if frame['action'][3] < 0:
|
||||
# frame['action'][3] += 2 * math.pi
|
||||
# if frame['observation.state'][3] < 0:
|
||||
# frame['observation.state'][3] += 2 * math.pi
|
||||
# return frame
|
||||
# else:
|
||||
# frame_callback = None
|
||||
frame_callback = None
|
||||
input('\nPress Enter to record this episode >>>>> ')
|
||||
time.sleep(0.5)
|
||||
teleop.set_ctrl_status(True)
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
frame_callback = None
|
||||
|
||||
with VideoEncodingManager(dataset):
|
||||
recorded_episodes = 0
|
||||
while recorded_episodes < cfg.dataset.num_episodes and not events["stop_recording"]:
|
||||
if teleop is not None and isinstance(teleop, UFMockTeleop):
|
||||
if events["stop_recording"]:
|
||||
continue
|
||||
teleop.configure(events=events)
|
||||
if events["rerecord_episode"]:
|
||||
events["rerecord_episode"] = False
|
||||
events["exit_early"] = False
|
||||
input('\nPress Enter to regenerate random target location >>>>> ')
|
||||
continue
|
||||
if events["stop_recording"]:
|
||||
continue
|
||||
|
||||
log_say(f"Recording episode {dataset.num_episodes}", cfg.play_sounds)
|
||||
record_loop(
|
||||
robot=robot,
|
||||
events=events,
|
||||
fps=cfg.dataset.fps,
|
||||
teleop_action_processor=teleop_action_processor,
|
||||
robot_action_processor=robot_action_processor,
|
||||
robot_observation_processor=robot_observation_processor,
|
||||
teleop=teleop,
|
||||
policy=policy,
|
||||
preprocessor=preprocessor,
|
||||
postprocessor=postprocessor,
|
||||
dataset=dataset,
|
||||
control_time_s=cfg.dataset.episode_time_s,
|
||||
single_task=cfg.dataset.single_task,
|
||||
display_data=cfg.display_data,
|
||||
frame_callback=frame_callback,
|
||||
)
|
||||
|
||||
if events["rerecord_episode"]:
|
||||
log_say("Re-record episode", cfg.play_sounds)
|
||||
events["rerecord_episode"] = False
|
||||
events["exit_early"] = False
|
||||
if isinstance(teleop, UFBaseTeleop):
|
||||
teleop.set_ctrl_status(False)
|
||||
dataset.clear_episode_buffer()
|
||||
input('\nPress Enter to rerecord this episode >>>>> ')
|
||||
|
||||
if isinstance(teleop, UFBaseTeleop):
|
||||
robot.configure()
|
||||
time.sleep(0.5)
|
||||
teleop.set_ctrl_status(True)
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
|
||||
if not events['stop_recording']:
|
||||
if isinstance(teleop, UFBaseTeleop):
|
||||
teleop.set_ctrl_status(False)
|
||||
dataset.save_episode()
|
||||
recorded_episodes += 1
|
||||
input('Press Enter to record at the next episode >>>>> ')
|
||||
if isinstance(teleop, UFBaseTeleop):
|
||||
robot.configure()
|
||||
time.sleep(1)
|
||||
teleop.set_ctrl_status(True)
|
||||
|
||||
print("\n********** Episode Record Loop Exit **********")
|
||||
|
||||
robot.disconnect()
|
||||
if teleop is not None:
|
||||
teleop.disconnect()
|
||||
|
||||
if not is_headless() and listener is not None:
|
||||
listener.stop()
|
||||
|
||||
if cfg.dataset.push_to_hub:
|
||||
dataset.push_to_hub(tags=cfg.dataset.tags, private=cfg.dataset.private)
|
||||
|
||||
log_say("Exiting", cfg.play_sounds)
|
||||
return dataset
|
||||
|
||||
|
||||
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',
|
||||
action='store_true', # specify --resume if resume needs to be True
|
||||
default=False,
|
||||
help='Whether contitue recording on existing dataset (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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
118
ufactory_lerobot/scripts/uf_robot_teleop.py
Normal file
118
ufactory_lerobot/scripts/uf_robot_teleop.py
Normal file
@ -0,0 +1,118 @@
|
||||
import yaml
|
||||
import argparse
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from dataclasses import asdict, dataclass
|
||||
from pprint import pformat
|
||||
import ufactory_lerobot # patch
|
||||
from lerobot.scripts.lerobot_record import register_third_party_plugins
|
||||
from lerobot.processor import (
|
||||
make_default_processors,
|
||||
)
|
||||
from lerobot.robots import ( # noqa: F401
|
||||
RobotConfig,
|
||||
make_robot_from_config,
|
||||
)
|
||||
from lerobot.teleoperators import ( # noqa: F401
|
||||
TeleoperatorConfig,
|
||||
make_teleoperator_from_config,
|
||||
)
|
||||
from lerobot.utils.control_utils import (
|
||||
is_headless,
|
||||
init_keyboard_listener
|
||||
)
|
||||
from lerobot.utils.import_utils import register_third_party_plugins
|
||||
from lerobot.utils.robot_utils import precise_sleep
|
||||
from lerobot.utils.utils import (
|
||||
init_logging,
|
||||
)
|
||||
from ufactory_lerobot.utils.utils import instantiate_from_dict
|
||||
|
||||
@dataclass
|
||||
class TeleopConfig:
|
||||
robot: RobotConfig
|
||||
teleop: TeleoperatorConfig
|
||||
fps: int = 30
|
||||
|
||||
|
||||
def teleop_loop(cfg: TeleopConfig):
|
||||
init_logging()
|
||||
logging.info(pformat(asdict(cfg)))
|
||||
|
||||
teleop = make_teleoperator_from_config(cfg.teleop)
|
||||
if hasattr(cfg.robot, "teleop"):
|
||||
cfg.robot.teleop = teleop
|
||||
robot = make_robot_from_config(cfg.robot)
|
||||
|
||||
teleop_action_processor, robot_action_processor, robot_observation_processor = make_default_processors()
|
||||
|
||||
robot.connect()
|
||||
teleop.connect()
|
||||
|
||||
events = {"exit": False}
|
||||
listener = None
|
||||
|
||||
if not is_headless():
|
||||
from pynput import keyboard
|
||||
|
||||
def on_press(key):
|
||||
try:
|
||||
if key == keyboard.Key.esc:
|
||||
print("Escape key pressed. Stopping ...")
|
||||
events["exit"] = True
|
||||
except Exception as e:
|
||||
print(f"Error handling key press: {e}")
|
||||
|
||||
listener, events = init_keyboard_listener(events=events, on_press=on_press)
|
||||
|
||||
sleep_time_s = 1 / cfg.fps
|
||||
|
||||
print("\n********** Test Teleop With Robot **********")
|
||||
input('Enter to control robot with teleop >>> ')
|
||||
|
||||
print("\n********** Teleop Control Loop Start **********")
|
||||
|
||||
while not events["exit"]:
|
||||
start_loop_t = time.perf_counter()
|
||||
|
||||
# Get robot observation
|
||||
obs = robot.get_observation()
|
||||
|
||||
act = teleop.get_action()
|
||||
act_processed_teleop = teleop_action_processor((act, obs))
|
||||
|
||||
robot_action_to_send = robot_action_processor((act_processed_teleop, obs))
|
||||
robot.send_action(robot_action_to_send)
|
||||
|
||||
dt_s = time.perf_counter() - start_loop_t
|
||||
precise_sleep(sleep_time_s - dt_s)
|
||||
|
||||
print("\n********** Teleop Control Loop Exit **********")
|
||||
robot.disconnect()
|
||||
teleop.disconnect()
|
||||
if not is_headless() and listener is not None:
|
||||
listener.stop()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
ufactory_lerobot/teleoperators/__init__.py
Normal file
0
ufactory_lerobot/teleoperators/__init__.py
Normal file
1
ufactory_lerobot/teleoperators/base_teleop/__init__.py
Normal file
1
ufactory_lerobot/teleoperators/base_teleop/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .base_teleop import UFBaseTeleop
|
||||
13
ufactory_lerobot/teleoperators/base_teleop/base_teleop.py
Normal file
13
ufactory_lerobot/teleoperators/base_teleop/base_teleop.py
Normal file
@ -0,0 +1,13 @@
|
||||
from lerobot.teleoperators import Teleoperator
|
||||
|
||||
|
||||
class UFBaseTeleop(Teleoperator):
|
||||
config_class = None
|
||||
name = "Base Teleop For UFACTORY"
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
|
||||
def set_ctrl_status(self, status):
|
||||
pass
|
||||
4
ufactory_lerobot/teleoperators/gello_teleop/__init__.py
Normal file
4
ufactory_lerobot/teleoperators/gello_teleop/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from .gello_teleop_config import GelloTeleopConfig
|
||||
from .gello_teleop import GelloTeleop
|
||||
136
ufactory_lerobot/teleoperators/gello_teleop/gello_teleop.py
Normal file
136
ufactory_lerobot/teleoperators/gello_teleop/gello_teleop.py
Normal file
@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python
|
||||
import logging
|
||||
import time
|
||||
import numpy as np
|
||||
from gello.dynamixel.driver import DynamixelDriver
|
||||
from gello.agents.gello_agent import GelloAgent, DynamixelRobotConfig
|
||||
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
|
||||
from ..base_teleop import UFBaseTeleop
|
||||
from .gello_teleop_config import GelloTeleopConfig
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GelloTeleop(UFBaseTeleop):
|
||||
"""
|
||||
GELLO for xArm tele-op, ref: https://wuphilipp.github.io/gello_site/
|
||||
"""
|
||||
|
||||
config_class = GelloTeleopConfig
|
||||
name = "Gello Teleop For xArm"
|
||||
|
||||
def __init__(self, config: GelloTeleopConfig):
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
self._is_connected = False
|
||||
self._is_calibrated = True # CHECK!!
|
||||
|
||||
# auto get joint offset from gello
|
||||
joint_ids = []
|
||||
joint_ids.extend(self.config.joint_ids)
|
||||
if self.config.gripper_id >= 0:
|
||||
joint_ids.append(self.config.gripper_id)
|
||||
driver = DynamixelDriver(joint_ids, port=self.config.port, baudrate=57600)
|
||||
for _ in range(10):
|
||||
driver.get_joints() # warmup
|
||||
curr_joints = driver.get_joints()
|
||||
driver.close()
|
||||
joint_offsets = []
|
||||
for i in range(len(self.config.start_joints)):
|
||||
offset = curr_joints[i] - self.config.start_joints[i] / self.config.joint_signs[i]
|
||||
joint_offsets.append(offset)
|
||||
if self.config.gripper_id >= 0:
|
||||
gripper_config = [self.config.gripper_id, np.rad2deg(curr_joints[-1]) - 0.2, np.rad2deg(curr_joints[-1]) - 42]
|
||||
else:
|
||||
gripper_config = None
|
||||
|
||||
param_dict = {
|
||||
"joint_ids": self.config.joint_ids,
|
||||
"joint_signs": self.config.joint_signs,
|
||||
"joint_offsets": joint_offsets,
|
||||
"gripper_config": gripper_config
|
||||
}
|
||||
self._dynamixel_robo_config = DynamixelRobotConfig(**param_dict)
|
||||
print(self._dynamixel_robo_config)
|
||||
self.dof = len(self.config.start_joints)
|
||||
|
||||
if self.config.torque_joint_ids:
|
||||
driver = DynamixelDriver(self.config.torque_joint_ids, port=self.config.port, baudrate=57600)
|
||||
driver.set_torque_mode(True)
|
||||
driver.close()
|
||||
|
||||
@property
|
||||
def action_features(self) -> dict:
|
||||
# Add one more dof for gripper
|
||||
# act_ft = {
|
||||
# "joint_position": {
|
||||
# "dtype": "float",
|
||||
# "shape": (self.dof+1,)
|
||||
# }
|
||||
# }
|
||||
act_ft = { f"J{i+1}.pos": float for i in range(self.dof) } | {"gripper.pos": float}
|
||||
return act_ft
|
||||
|
||||
@property
|
||||
def feedback_features(self) -> dict:
|
||||
# fbk_ft = {
|
||||
# "joint_position": {
|
||||
# "dtype": "float",
|
||||
# "shape": (self.dof+1,)
|
||||
# }
|
||||
# }
|
||||
fbk_ft = { f"J{i+1}.pos": float for i in range(self.dof) } | {"gripper.pos": float}
|
||||
return fbk_ft
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self._is_connected
|
||||
|
||||
def connect(self, calibrate: bool = True) -> None:
|
||||
if self._is_connected:
|
||||
raise DeviceAlreadyConnectedError(f"{self} already connected")
|
||||
|
||||
self.gello_agent = GelloAgent(port=self.config.port, dynamixel_config=self._dynamixel_robo_config)
|
||||
if not self._is_calibrated and calibrate:
|
||||
logger.info(
|
||||
"Mismatch between calibration values in the motor and the calibration file or no calibration file found"
|
||||
)
|
||||
self.calibrate()
|
||||
|
||||
self.configure()
|
||||
self._is_connected = True
|
||||
logger.info(f"{self} connected.")
|
||||
|
||||
@property
|
||||
def is_calibrated(self) -> bool:
|
||||
return self._is_calibrated
|
||||
|
||||
def calibrate(self) -> None:
|
||||
pass
|
||||
|
||||
def configure(self) -> None:
|
||||
# TODO: Go to sync position slowly? Can not
|
||||
pass
|
||||
|
||||
def get_action(self) -> dict[str, np.ndarray]:
|
||||
start = time.perf_counter()
|
||||
fake_obs = dict({"joint_state": np.array([0.0]*(self.dof+1))}) # for agent.act() argument, actually no use
|
||||
action_array = self.gello_agent.act(fake_obs) # current gello joint pos as np.ndarray
|
||||
dt_ms = (time.perf_counter() - start) * 1e3
|
||||
logger.debug(f"{self} read action: {dt_ms:.1f}ms")
|
||||
|
||||
action = {}
|
||||
for i in range(self.dof):
|
||||
action.update({f"J{i+1}.pos": action_array[i]})
|
||||
action.update({"gripper.pos": action_array[self.dof]})
|
||||
return action
|
||||
|
||||
def send_feedback(self, feedback: dict[str, float]) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def disconnect(self) -> None:
|
||||
if not self._is_connected:
|
||||
DeviceNotConnectedError(f"{self} is not connected.")
|
||||
|
||||
self._is_connected = False
|
||||
logger.info(f"{self} disconnected.")
|
||||
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import numpy as np
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple
|
||||
from lerobot.teleoperators import TeleoperatorConfig
|
||||
|
||||
|
||||
@TeleoperatorConfig.register_subclass("uf::gello_teleop")
|
||||
@dataclass
|
||||
class GelloTeleopConfig(TeleoperatorConfig):
|
||||
# Port to connect to the gello dummy arm
|
||||
port: str = "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0"
|
||||
|
||||
# Others: Calibration angles, joint directions etc
|
||||
joint_ids: Tuple[int, ...] = (1, 2, 3, 4, 5, 6, 7)
|
||||
joint_signs: Tuple[int, ...] = (1, 1, 1, 1, 1, 1, 1) # if follow the original open-sourced gello xarm7 setup
|
||||
start_joints: Tuple[float, ...] = (0, 0, 0, np.pi/2, 0, np.pi/2, 0)
|
||||
gripper_id: int = 8 # -1: no gripper
|
||||
torque_joint_ids: Tuple[int, ...] = None # the joints will activate torque mode.
|
||||
4
ufactory_lerobot/teleoperators/pika_teleop/__init__.py
Normal file
4
ufactory_lerobot/teleoperators/pika_teleop/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from .pika_teleop_config import PikaTeleopConfig
|
||||
from .pika_teleop import PikaTeleop
|
||||
263
ufactory_lerobot/teleoperators/pika_teleop/pika_teleop.py
Normal file
263
ufactory_lerobot/teleoperators/pika_teleop/pika_teleop.py
Normal file
@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import time
|
||||
import math
|
||||
import numpy as np
|
||||
from typing import Any
|
||||
from threading import Thread, Event, Lock
|
||||
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
|
||||
from xarm.wrapper import XArmAPI
|
||||
from ufactory_lerobot.devices.pika import PikaDevice
|
||||
from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations
|
||||
from ..base_teleop import UFBaseTeleop
|
||||
from .pika_teleop_config import PikaTeleopConfig
|
||||
|
||||
|
||||
class PikaTeleop(UFBaseTeleop, Thread):
|
||||
|
||||
config_class = PikaTeleopConfig
|
||||
name = "Pika Teleop For xArm"
|
||||
|
||||
def __init__(self, config: PikaTeleopConfig):
|
||||
|
||||
super().__init__(config)
|
||||
Thread.__init__(self) # Do NOT REMOVE!
|
||||
self.stop_event = Event()
|
||||
self.config = config
|
||||
self._is_connected = False
|
||||
self._is_calibrated = True
|
||||
self._data_lock = Lock()
|
||||
self._ctrl_flag = False
|
||||
self._need_initial = False
|
||||
|
||||
self.pika_device = PikaDevice(1, pika_sense_port=self.config.port)
|
||||
self.pika_sense = self.pika_device.pika_sense
|
||||
|
||||
if self.config.robot_ip:
|
||||
self.arm = XArmAPI(self.config.robot_ip, is_radian=True)
|
||||
else:
|
||||
self.arm = None
|
||||
|
||||
self._robot_target_pose = None
|
||||
self._gripper_target_pos = None
|
||||
|
||||
@property
|
||||
def action_features(self) -> dict:
|
||||
if self.config.use_gripper:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (7,),
|
||||
"names": {"pose.x": 0, "pose.y": 1, "pose.z": 2, "pose.rx": 3, "pose.ry": 4, "pose.rz": 5, "gripper.pos": 6},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (6,),
|
||||
"names": {"pose.x": 0, "pose.y": 1, "pose.z": 2, "pose.rx": 3, "pose.ry": 4, "pose.rz": 5},
|
||||
}
|
||||
|
||||
@property
|
||||
def feedback_features(self) -> dict:
|
||||
if self.config.use_gripper:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (7,),
|
||||
"names": {"pose.x": 0, "pose.y": 1, "pose.z": 2, "pose.rx": 3, "pose.ry": 4, "pose.rz": 5, "gripper.pos": 6},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (6,),
|
||||
"names": {"pose.x": 0, "pose.y": 1, "pose.z": 2, "pose.rx": 3, "pose.ry": 4, "pose.rz": 5},
|
||||
}
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self._is_connected
|
||||
|
||||
@property
|
||||
def is_calibrated(self) -> bool:
|
||||
return self._is_calibrated
|
||||
|
||||
def calibrate(self) -> None:
|
||||
# CHECK!!
|
||||
pass
|
||||
|
||||
def configure(self) -> None:
|
||||
pass
|
||||
|
||||
def connect(self, calibrate: bool = False) -> None:
|
||||
self.start()
|
||||
|
||||
def disconnect(self):
|
||||
if not self._is_connected:
|
||||
DeviceNotConnectedError(f"{self} is not connected.")
|
||||
|
||||
self.stop_event.set()
|
||||
self._is_connected = False
|
||||
self.join()
|
||||
|
||||
def set_ctrl_status(self, status):
|
||||
if status:
|
||||
if not self._ctrl_flag:
|
||||
print('开始遥操作')
|
||||
self._ctrl_flag = True
|
||||
self._need_initial = True
|
||||
else:
|
||||
self._ctrl_flag = False
|
||||
self._need_initial = False
|
||||
print('停止遥操作')
|
||||
|
||||
def run(self):
|
||||
self._is_connected = True
|
||||
init_state = self.pika_sense.get_command_state()
|
||||
curr_state = init_state
|
||||
|
||||
last_gripper_distance = 0
|
||||
|
||||
self._ctrl_flag = False # 是否开启遥操作
|
||||
self._need_initial = False
|
||||
|
||||
sleep_time = 1 / self.config.frequency
|
||||
|
||||
if self.arm:
|
||||
self.arm.set_linear_spd_limit_factor(2.0)
|
||||
|
||||
pika_to_robot_eef = [0, 0, 0, math.pi, -math.pi / 2, 0] # rpy
|
||||
# pika_to_robot_eef = [0, 0, 0, math.pi, 0, 0]
|
||||
|
||||
# pika坐标系到机械臂坐标系的变换关系对应的变换矩阵
|
||||
pika_to_robot_matrix = Transformations.xyzrpy_to_rotation_matrix(*pika_to_robot_eef)
|
||||
# 机械臂初始位置对应的变换矩阵
|
||||
# robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*[0, 0, 190, -np.pi, -np.radians(41), 0])
|
||||
robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*[300, 0, 365, np.pi, 0, 0])
|
||||
# pika初始位置转换到机械臂坐标系后对应的变换矩阵
|
||||
pika_begin_robot_matrix = None
|
||||
# pika目标位置转换到机械臂坐标系后对应的变换矩阵
|
||||
pika_end_robot_matrix = None
|
||||
|
||||
scale_xyz = self.config.scale_xyz
|
||||
|
||||
while not self.stop_event.is_set():
|
||||
time.sleep(sleep_time)
|
||||
|
||||
if not self.arm and pika_begin_robot_matrix is None:
|
||||
pose = self.pika_sense.get_pose(self.pika_device.pika_tracker_device)
|
||||
if not pose:
|
||||
continue
|
||||
x, y, z = pose.position[0] * 1000 * scale_xyz, pose.position[1] * 1000 * scale_xyz, pose.position[2] * 1000 * scale_xyz
|
||||
pika_begin_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, pose.rotation, pika_to_robot_matrix)
|
||||
robot_target_pose = Transformations.tracker_robot_matrix_to_robot_pose(pika_begin_robot_matrix, pika_begin_robot_matrix, robot_base_matrix, is_axis_angle=True)
|
||||
print('初始绑定, 当前Pika位置对应的机械臂目标位置: x={:.6f}, y={:.6f}, z={:.6f}, rx={:.6f}, ry={:.6f}, rz={:.6f}'.format(robot_target_pose[0], robot_target_pose[1], robot_target_pose[2], math.degrees(robot_target_pose[3]), math.degrees(robot_target_pose[4]), math.degrees(robot_target_pose[5])))
|
||||
continue
|
||||
|
||||
state = self.pika_sense.get_command_state()
|
||||
if state != curr_state:
|
||||
curr_state = state
|
||||
if not self._ctrl_flag and curr_state != init_state:
|
||||
self._ctrl_flag = True
|
||||
self._need_initial = True
|
||||
# self.robot_init()
|
||||
print('开始遥操作')
|
||||
time.sleep(1)
|
||||
elif self._ctrl_flag and curr_state == init_state:
|
||||
self._ctrl_flag = False
|
||||
print('停止遥操作')
|
||||
continue
|
||||
|
||||
if self._ctrl_flag and self.arm and (not self.arm.connected or self.arm.error_code != 0 or self.arm.state >= 4):
|
||||
print('机械臂原因, 遥操作自动停止')
|
||||
init_state = state
|
||||
curr_state = state
|
||||
self._ctrl_flag = False
|
||||
continue
|
||||
|
||||
if not self._ctrl_flag:
|
||||
continue
|
||||
|
||||
if self.config.use_gripper:
|
||||
distance = min(max(self.pika_sense.get_gripper_distance(), 0), 100)
|
||||
|
||||
if abs(last_gripper_distance - distance) > 2:
|
||||
last_gripper_distance = distance
|
||||
with self._data_lock:
|
||||
self._gripper_target_pos = last_gripper_distance
|
||||
|
||||
pose = self.pika_sense.get_pose(self.pika_device.pika_tracker_device)
|
||||
if not pose:
|
||||
continue
|
||||
x, y, z = pose.position[0] * 1000 * scale_xyz, pose.position[1] * 1000 * scale_xyz, pose.position[2] * 1000 * scale_xyz
|
||||
|
||||
if not self.arm:
|
||||
# 只有PIKA设备, 没有机械臂
|
||||
pika_end_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, pose.rotation, pika_to_robot_matrix)
|
||||
robot_target_pose = Transformations.tracker_robot_matrix_to_robot_pose(pika_begin_robot_matrix, pika_end_robot_matrix, robot_base_matrix, is_axis_angle=True)
|
||||
|
||||
if self._need_initial:
|
||||
self._need_initial = False
|
||||
print('[初始位置] x={:.6f}, y={:.6f}, z={:.6f}, rx={:.6f}, ry={:.6f}, rz={:.6f}'.format(robot_target_pose[0], robot_target_pose[1], robot_target_pose[2], math.degrees(robot_target_pose[3]), math.degrees(robot_target_pose[4]), math.degrees(robot_target_pose[5])))
|
||||
else:
|
||||
if self._need_initial:
|
||||
self._need_initial = False
|
||||
# _, robot_pos = self.arm.get_position()
|
||||
_, robot_pos = self.arm.get_position(is_radian=True)
|
||||
robot_base_pose = robot_pos
|
||||
print('[初始] 机械臂位置: {}'.format(robot_pos))
|
||||
|
||||
# 机械臂初始位置对应的变换矩阵
|
||||
robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*robot_pos)
|
||||
|
||||
# pika初始位置转换到机械臂坐标系后对应的变换矩阵
|
||||
pika_begin_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, pose.rotation, pika_to_robot_matrix)
|
||||
pika_end_robot_matrix = pika_begin_robot_matrix
|
||||
else:
|
||||
# pika目标位置转换到机械臂坐标系后对应的变换矩阵
|
||||
pika_end_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, pose.rotation, pika_to_robot_matrix)
|
||||
|
||||
robot_target_pose = Transformations.tracker_robot_matrix_to_robot_pose(pika_begin_robot_matrix, pika_end_robot_matrix, robot_base_matrix, is_axis_angle=True)
|
||||
|
||||
with self._data_lock:
|
||||
self._robot_target_pose = robot_target_pose
|
||||
|
||||
# delta action
|
||||
def get_action(self) -> dict[str, Any]:
|
||||
if not self.is_connected:
|
||||
raise DeviceNotConnectedError(
|
||||
"PikaTeleop is not connected. You need to run `connect()` before `get_action()`."
|
||||
)
|
||||
|
||||
with self._data_lock:
|
||||
if self._robot_target_pose is not None:
|
||||
robot_target_pose = self._robot_target_pose.copy()
|
||||
else:
|
||||
robot_target_pose = None
|
||||
if self._gripper_target_pos is not None:
|
||||
gripper_target_pos = (100 - self._gripper_target_pos) / (100 - 0)
|
||||
else:
|
||||
gripper_target_pos = 0.0
|
||||
|
||||
if robot_target_pose is None:
|
||||
if self.arm:
|
||||
_, robot_target_pose = self.arm.get_position_aa(is_radian=True)
|
||||
else:
|
||||
# robot_target_pose = [0, 0, 190, -np.pi, -np.radians(41), 0]
|
||||
robot_target_pose = [300, 0, 365, np.pi, 0, 0]
|
||||
# print(self._robot_target_pose, robot_target_pose)
|
||||
|
||||
# output is delta change of the robot pose
|
||||
action_dict = {
|
||||
"pose.x": robot_target_pose[0],
|
||||
"pose.y": robot_target_pose[1],
|
||||
"pose.z": robot_target_pose[2],
|
||||
"pose.rx": robot_target_pose[3],
|
||||
"pose.ry": robot_target_pose[4],
|
||||
"pose.rz": robot_target_pose[5],
|
||||
}
|
||||
|
||||
if self.config.use_gripper:
|
||||
action_dict.update({"gripper.pos": gripper_target_pos})
|
||||
|
||||
return action_dict
|
||||
|
||||
def send_feedback(self, feedback: dict[str, float]) -> None:
|
||||
raise NotImplementedError
|
||||
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2025 UFACTORY Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from lerobot.teleoperators import TeleoperatorConfig
|
||||
|
||||
|
||||
@TeleoperatorConfig.register_subclass("uf::pika_teleop")
|
||||
@dataclass
|
||||
class PikaTeleopConfig(TeleoperatorConfig):
|
||||
# robot_ip to connect to the arm
|
||||
robot_ip: str = None
|
||||
# Port to connect to the pika
|
||||
port: str = None
|
||||
frequency: int = 100 # hz
|
||||
use_gripper: bool = True
|
||||
scale_xyz: float = 1.0 #
|
||||
rx_continuous: bool = False
|
||||
4
ufactory_lerobot/teleoperators/space_mouse/__init__.py
Normal file
4
ufactory_lerobot/teleoperators/space_mouse/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from .space_mouse_config import SpaceMouseTeleopConfig
|
||||
from .space_mouse import SpaceMouseTeleop
|
||||
187
ufactory_lerobot/teleoperators/space_mouse/space_mouse.py
Normal file
187
ufactory_lerobot/teleoperators/space_mouse/space_mouse.py
Normal file
@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import time
|
||||
import numpy as np
|
||||
from typing import Any
|
||||
from threading import Thread, Event
|
||||
from spnav import spnav_open, spnav_poll_event, spnav_close, SpnavMotionEvent, SpnavButtonEvent
|
||||
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
|
||||
from collections import defaultdict
|
||||
|
||||
from lerobot.teleoperators import Teleoperator
|
||||
from .space_mouse_config import SpaceMouseTeleopConfig
|
||||
|
||||
class SpaceMouseTeleop(Teleoperator, Thread):
|
||||
|
||||
config_class = SpaceMouseTeleopConfig
|
||||
name = "Space Mouse Teleop For xArm"
|
||||
|
||||
def __init__(self, config: SpaceMouseTeleopConfig):
|
||||
|
||||
super().__init__(config)
|
||||
Thread.__init__(self) # Do NOT REMOVE!
|
||||
self.stop_event = Event()
|
||||
self.config = config
|
||||
self.max_value = config.max_value
|
||||
self.frequency = config.frequency
|
||||
self.max_pos_speed = config.max_pos_speed
|
||||
deadzone = config.deadzone
|
||||
self._is_connected = False
|
||||
self.dtype = np.float32 # CHECK! make it configurable ???
|
||||
|
||||
if np.issubdtype(type(deadzone), np.number):
|
||||
self.deadzone = np.full(6, fill_value=deadzone, dtype=self.dtype)
|
||||
else:
|
||||
self.deadzone = np.array(deadzone, dtype=self.dtype)
|
||||
assert (self.deadzone >= 0).all()
|
||||
|
||||
self.motion_event = SpnavMotionEvent([0,0,0], [0,0,0], 0)
|
||||
self.button_state = defaultdict(lambda: False)
|
||||
self.tx_zup_spnav = np.array([
|
||||
[0,0,-1],
|
||||
[1,0,0],
|
||||
[0,1,0]
|
||||
], dtype=np.float32)
|
||||
|
||||
@property
|
||||
def action_features(self) -> dict:
|
||||
if self.config.use_gripper:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (4,),
|
||||
"names": {"pose.dx": 0, "pose.dy": 1, "pose.dz": 2, "gripper.pos": 3},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (3,),
|
||||
"names": {"pose.dx": 0, "pose.dy": 1, "pose.dz": 2},
|
||||
}
|
||||
|
||||
@property
|
||||
def feedback_features(self) -> dict:
|
||||
if self.config.use_gripper:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (4,),
|
||||
"names": {"pose.dx": 0, "pose.dy": 1, "pose.dz": 2, "gripper.pos": 3},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (3,),
|
||||
"names": {"pose.dx": 0, "pose.dy": 1, "pose.dz": 2},
|
||||
}
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self._is_connected
|
||||
|
||||
@property
|
||||
def is_calibrated(self) -> bool:
|
||||
return self._is_calibrated
|
||||
|
||||
def calibrate(self) -> None:
|
||||
# CHECK!!
|
||||
pass
|
||||
|
||||
def configure(self) -> None:
|
||||
pass
|
||||
|
||||
def connect(self, calibrate: bool = False) -> None:
|
||||
self.start()
|
||||
|
||||
def disconnect(self):
|
||||
if not self._is_connected:
|
||||
DeviceNotConnectedError(f"{self} is not connected.")
|
||||
|
||||
self.stop_event.set()
|
||||
self._is_connected = False
|
||||
self.join()
|
||||
|
||||
def get_motion_state(self):
|
||||
me = self.motion_event
|
||||
state = np.array(me.translation + me.rotation,
|
||||
dtype=self.dtype) / self.max_value
|
||||
is_dead = (-self.deadzone < state) & (state < self.deadzone)
|
||||
state[is_dead] = 0
|
||||
return state
|
||||
|
||||
def get_motion_state_transformed(self):
|
||||
"""
|
||||
Return in right-handed coordinate
|
||||
z
|
||||
*------>y right
|
||||
| _
|
||||
| (O) space mouse
|
||||
v
|
||||
x
|
||||
back
|
||||
|
||||
"""
|
||||
state = self.get_motion_state()
|
||||
tf_state = np.zeros_like(state)
|
||||
tf_state[:3] = self.tx_zup_spnav @ state[:3]
|
||||
tf_state[3:] = self.tx_zup_spnav @ state[3:]
|
||||
return tf_state
|
||||
|
||||
def is_button_pressed(self, button_id):
|
||||
return self.button_state[button_id]
|
||||
|
||||
def run(self):
|
||||
spnav_open()
|
||||
self._is_connected = True
|
||||
try:
|
||||
while not self.stop_event.is_set():
|
||||
event = spnav_poll_event()
|
||||
if isinstance(event, SpnavMotionEvent):
|
||||
self.motion_event = event
|
||||
elif isinstance(event, SpnavButtonEvent):
|
||||
self.button_state[event.bnum] = event.press
|
||||
else:
|
||||
time.sleep(1/200)
|
||||
finally:
|
||||
self._is_connected = False
|
||||
spnav_close()
|
||||
|
||||
# delta action
|
||||
def get_action(self) -> dict[str, Any]:
|
||||
if not self.is_connected:
|
||||
raise DeviceNotConnectedError(
|
||||
"SpaceMouseTeleop is not connected. You need to run `connect()` before `get_action()`."
|
||||
)
|
||||
|
||||
# self._drain_pressed_keys()
|
||||
sm_state = self.get_motion_state_transformed()
|
||||
|
||||
dpos = sm_state[:3] * self.max_pos_speed / self.frequency
|
||||
|
||||
# Currently No rotation operation
|
||||
# drot_xyz = sm_state[3:] * (max_rot_speed / frequency)
|
||||
|
||||
# if not self.is_button_pressed(0):
|
||||
# # translation mode
|
||||
# drot_xyz[:] = 0
|
||||
# else:
|
||||
# dpos[:] = 0
|
||||
# if not self.is_button_pressed(1):
|
||||
|
||||
# X-Y 2D translation mode, no gripper control. Modify the code if you need more DOF control
|
||||
dpos[2] = 0
|
||||
|
||||
gripper_action = 1.0
|
||||
|
||||
# output is delta change of the robot pose
|
||||
action_dict = {
|
||||
"pose.dx": dpos[0],
|
||||
"pose.dy": dpos[1],
|
||||
"pose.dz": dpos[2],
|
||||
}
|
||||
|
||||
if self.config.use_gripper:
|
||||
action_dict.update({"gripper.pos": gripper_action})
|
||||
|
||||
return action_dict
|
||||
|
||||
def send_feedback(self, feedback: dict[str, float]) -> None:
|
||||
raise NotImplementedError
|
||||
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2025 UFACTORY Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from lerobot.teleoperators import TeleoperatorConfig
|
||||
|
||||
|
||||
@TeleoperatorConfig.register_subclass("uf::spacemouse_teleop")
|
||||
@dataclass
|
||||
class SpaceMouseTeleopConfig(TeleoperatorConfig):
|
||||
# Port to connect to the arm
|
||||
max_value: int = 300
|
||||
deadzone: tuple = (0,0,0,0,0,0)
|
||||
use_gripper: bool = False
|
||||
frequency: int = 10 # hz
|
||||
max_pos_speed: int = 250 # mm/s
|
||||
# Others: Calibration angles, joint directions etc.
|
||||
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from .uf_mock_teleop_config import UFMockTeleopConfig
|
||||
from .uf_mock_teleop import UFMockTeleop
|
||||
479
ufactory_lerobot/teleoperators/uf_mock_teleop/uf_mock_teleop.py
Normal file
479
ufactory_lerobot/teleoperators/uf_mock_teleop/uf_mock_teleop.py
Normal file
@ -0,0 +1,479 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import time
|
||||
import socket
|
||||
import random
|
||||
from typing import Any
|
||||
import threading
|
||||
from xarm.wrapper import XArmAPI
|
||||
from xarm.core.utils import convert
|
||||
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
|
||||
|
||||
from lerobot.teleoperators import Teleoperator
|
||||
from .uf_mock_teleop_config import UFMockTeleopConfig
|
||||
|
||||
RECORD_NONE = 0
|
||||
RECORD_POSE = RECORD_NO_GRIPPER = 1
|
||||
RECORD_GRIPPER = 2
|
||||
RECORD_POSE_GRIPPER = RECORD_POSE | RECORD_GRIPPER
|
||||
|
||||
|
||||
class UFMockTeleop(Teleoperator, threading.Thread):
|
||||
|
||||
config_class = UFMockTeleopConfig
|
||||
name = "Mock Teleop For xArm"
|
||||
|
||||
def __init__(self, config: UFMockTeleopConfig):
|
||||
|
||||
super().__init__(config)
|
||||
threading.Thread.__init__(self) # Do NOT REMOVE!
|
||||
self.config = config
|
||||
self._is_connected = False
|
||||
self._is_calibrated = True # CHECK!!
|
||||
self._is_joint_space = config.control_space == 'joint'
|
||||
self._action_datas = []
|
||||
|
||||
self.arm = XArmAPI(config.robot_ip, do_not_open=True)
|
||||
self.rt_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
assert len(self.config.mock_x_range) >= 1, 'The length of the parameter mock_x_range cannot be less than 1.'
|
||||
assert len(self.config.mock_y_range) >= 1, 'The length of the parameter mock_y_range cannot be less than 1.'
|
||||
assert len(self.config.mock_z_range) >= 1, 'The length of the parameter mock_z_range cannot be less than 1.'
|
||||
assert len(self.config.mock_roll_range) >= 1, 'The length of the parameter mock_roll_range cannot be less than 1.'
|
||||
assert len(self.config.mock_pitch_range) >= 1, 'The length of the parameter mock_pitch_range cannot be less than 1.'
|
||||
assert len(self.config.mock_yaw_range) >= 1, 'The length of the parameter mock_yaw_range cannot be less than 1.'
|
||||
|
||||
self.mock_x_range = self.config.mock_x_range if len(self.config.mock_x_range) > 1 else (self.config.mock_x_range[0], self.config.mock_x_range[0])
|
||||
self.mock_y_range = self.config.mock_y_range if len(self.config.mock_y_range) > 1 else (self.config.mock_y_range[0], self.config.mock_y_range[0])
|
||||
self.mock_z_range = self.config.mock_z_range if len(self.config.mock_z_range) > 1 else (self.config.mock_z_range[0], self.config.mock_z_range[0])
|
||||
self.mock_roll_range = self.config.mock_roll_range if len(self.config.mock_roll_range) > 1 else (self.config.mock_roll_range[0], self.config.mock_roll_range[0])
|
||||
self.mock_pitch_range = self.config.mock_pitch_range if len(self.config.mock_pitch_range) > 1 else (self.config.mock_pitch_range[0], self.config.mock_pitch_range[0])
|
||||
self.mock_yaw_range = self.config.mock_yaw_range if len(self.config.mock_yaw_range) > 1 else (self.config.mock_yaw_range[0], self.config.mock_yaw_range[0])
|
||||
|
||||
self._last_mock_x = self.mock_x_range[0]
|
||||
self._last_mock_y = self.mock_y_range[0]
|
||||
self._last_mock_z = self.mock_z_range[0]
|
||||
self._last_mock_roll = self.mock_roll_range[0]
|
||||
self._last_mock_pitch = self.mock_pitch_range[0]
|
||||
self._last_mock_yaw = self.mock_yaw_range[0]
|
||||
|
||||
self._update_lock = threading.Lock()
|
||||
self._action_inx = 0
|
||||
self._record_status = 0 # 0: not recording; 1: recording joint/tcp pose; 3: recording joint/tcp pose and gripper pos
|
||||
self._report_gripper = False
|
||||
|
||||
def __set_record_status(self, status: int):
|
||||
self._record_status = status
|
||||
|
||||
def __reset_action_data(self):
|
||||
self.__set_record_status(RECORD_NONE)
|
||||
self._action_datas.clear()
|
||||
|
||||
def __init_robot(self):
|
||||
self.arm.connect(self.config.robot_ip)
|
||||
self.arm.motion_enable()
|
||||
self.arm.set_mode(0)
|
||||
self.arm.set_state(0)
|
||||
|
||||
if self.config.initial_tcp_pose is not None:
|
||||
self.arm.set_position(*self.config.initial_tcp_pose, speed=self.config.mock_tcp_speed, wait=True)
|
||||
if self.config.gripper_type > 0:
|
||||
self.arm.set_gripper_enable(True)
|
||||
self.arm.set_gripper_speed(3000)
|
||||
self.arm.set_gripper_position(800, wait=True)
|
||||
|
||||
if self.arm.arm.version_is_ge(2, 7, 100) and hasattr(self.arm, 'set_external_device_monitor_params'):
|
||||
self.arm.set_external_device_monitor_params(self.config.gripper_type, self.config.gripper_freq)
|
||||
self._report_gripper = True
|
||||
|
||||
self.rt_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.rt_sock.setblocking(True)
|
||||
self.rt_sock.settimeout(1)
|
||||
self.rt_sock.connect((self.config.robot_ip, 30000))
|
||||
self._is_connected = True
|
||||
|
||||
def __mock(self, events=None) -> bool:
|
||||
mock_tcp_speed = self.config.mock_tcp_speed
|
||||
grasp_tcp_speed = self.config.grasp_tcp_speed
|
||||
initial_tcp_pose = self.config.initial_tcp_pose
|
||||
self.arm.set_mode(0)
|
||||
self.arm.set_state(0)
|
||||
self.arm.set_position(*initial_tcp_pose, speed=mock_tcp_speed)
|
||||
if self.config.gripper_type > 0:
|
||||
self.arm.set_gripper_position(800, wait=True)
|
||||
|
||||
if abs(self._last_mock_x - self.mock_x_range[0]) >= abs(self._last_mock_x - self.mock_x_range[1]):
|
||||
mock_x_range = [self.mock_x_range[0], self._last_mock_x]
|
||||
else:
|
||||
mock_x_range = [self._last_mock_x, self.mock_x_range[1]]
|
||||
if abs(self._last_mock_y - self.mock_y_range[0]) >= abs(self._last_mock_y - self.mock_y_range[1]):
|
||||
mock_y_range = [self.mock_y_range[0], self._last_mock_y]
|
||||
else:
|
||||
mock_y_range = [self._last_mock_y, self.mock_y_range[1]]
|
||||
if abs(self._last_mock_z - self.mock_z_range[0]) >= abs(self._last_mock_z - self.mock_z_range[1]):
|
||||
mock_z_range = [self.mock_z_range[0], self._last_mock_z]
|
||||
else:
|
||||
mock_z_range = [self._last_mock_z, self.mock_z_range[1]]
|
||||
if abs(self._last_mock_roll - self.mock_roll_range[0]) >= abs(self._last_mock_roll - self.mock_roll_range[1]):
|
||||
mock_roll_range = [self.mock_roll_range[0], self._last_mock_roll]
|
||||
else:
|
||||
mock_roll_range = [self._last_mock_roll, self.mock_roll_range[1]]
|
||||
if abs(self._last_mock_pitch - self.mock_pitch_range[0]) >= abs(self._last_mock_pitch - self.mock_pitch_range[1]):
|
||||
mock_pitch_range = [self.mock_pitch_range[0], self._last_mock_pitch]
|
||||
else:
|
||||
mock_pitch_range = [self._last_mock_pitch, self.mock_pitch_range[1]]
|
||||
if abs(self._last_mock_yaw - self.mock_yaw_range[0]) >= abs(self._last_mock_yaw - self.mock_yaw_range[1]):
|
||||
mock_yaw_range = [self.mock_yaw_range[0], self._last_mock_yaw]
|
||||
else:
|
||||
mock_yaw_range = [self._last_mock_yaw, self.mock_yaw_range[1]]
|
||||
|
||||
x = random.uniform(mock_x_range[0], mock_x_range[1])
|
||||
y = random.uniform(mock_y_range[0], mock_y_range[1])
|
||||
z = random.uniform(mock_z_range[0], mock_z_range[1])
|
||||
roll = random.uniform(mock_roll_range[0], mock_roll_range[1])
|
||||
pitch = random.uniform(mock_pitch_range[0], mock_pitch_range[1])
|
||||
yaw = random.uniform(mock_yaw_range[0], mock_yaw_range[1])
|
||||
|
||||
# x = random.uniform(self.mock_x_range[0], self.mock_x_range[1])
|
||||
# y = random.uniform(self.mock_y_range[0], self.mock_y_range[1])
|
||||
# z = random.uniform(self.mock_z_range[0], self.mock_z_range[1])
|
||||
# roll = random.uniform(self.mock_roll_range[0], self.mock_roll_range[1])
|
||||
# pitch = random.uniform(self.mock_pitch_range[0], self.mock_pitch_range[1])
|
||||
# yaw = random.uniform(self.mock_yaw_range[0], self.mock_yaw_range[1])
|
||||
|
||||
print(f'\n[MOCK] x: {x:.1f} mm, y: {y:.1f} mm, z: {z:.1f} mm, roll: {roll:.1f} °, pitch: {pitch:.1f} °, yaw: {yaw:.1f} °')
|
||||
self._last_mock_x = x
|
||||
self._last_mock_y = y
|
||||
self._last_mock_z = z
|
||||
self._last_mock_roll = roll
|
||||
self._last_mock_pitch = pitch
|
||||
self._last_mock_yaw = yaw
|
||||
|
||||
code = self.arm.set_position(x=x, y=y, z=z, roll=roll, pitch=pitch, yaw=yaw, speed=mock_tcp_speed, wait=True)
|
||||
if code != 0:
|
||||
print(f'[MOCK ERROR] Failed to move to mock position, error code: {code}')
|
||||
return False
|
||||
print('[MOCK] Reached mock position.')
|
||||
if events and events['exit_early']: # exit early
|
||||
return False
|
||||
print('*** Place the target in the correct gripping position, adjust the robotic arm height.')
|
||||
input('*** Enter to continue >>> ')
|
||||
if events and events['exit_early']: # exit early
|
||||
return False
|
||||
|
||||
self.arm.set_mode(0)
|
||||
self.arm.set_state(0)
|
||||
|
||||
_, tcp_target_pose = self.arm.get_position(is_radian=False)
|
||||
# 回到抓取目标正上方
|
||||
code = self.arm.set_position(z=z + self.config.hover_offset, speed=mock_tcp_speed, wait=True)
|
||||
if code or (events and events['exit_early']): # exit early
|
||||
return False
|
||||
_, tcp_hover_pose = self.arm.get_position(is_radian=False)
|
||||
|
||||
# 回到初始位置
|
||||
code = self.arm.set_position(*initial_tcp_pose, speed=mock_tcp_speed, wait=True)
|
||||
if code or (events and events['exit_early']): # exit early
|
||||
return False
|
||||
|
||||
##################### 模拟抓取 ############################
|
||||
|
||||
self.__reset_action_data()
|
||||
self._action_inx = 0
|
||||
self.__set_record_status(RECORD_POSE_GRIPPER)
|
||||
time.sleep(1.0)
|
||||
|
||||
case_type = self.config.mock_type if self.config.mock_type in [1, 2, 3] else 1
|
||||
x0, y0, yaw0 = tcp_hover_pose[0], tcp_hover_pose[1], tcp_hover_pose[5]
|
||||
dis = 100
|
||||
yaw_dis = 45
|
||||
spd = grasp_tcp_speed // 3 * 2
|
||||
x_range = [x0 - dis, x0 + dis]
|
||||
y_range = [y0 - dis, y0 + dis]
|
||||
yaw_range = [yaw0 - yaw_dis, yaw0 + yaw_dis]
|
||||
|
||||
if self.config.mock_type == 0:
|
||||
# 按一定概率随机执行方案1/2/3
|
||||
val = val = random.random()
|
||||
case_type = 1 if val < 0.66 else 2 if val < 0.88 else 3
|
||||
|
||||
cnt = 0
|
||||
if case_type == 1:
|
||||
# 方案1: 不做额外处理
|
||||
pass
|
||||
elif case_type == 2:
|
||||
# 方案2: 在目标点位上方固定高度一定范围内游走
|
||||
cnt = random.randint(3, 8)
|
||||
for i in range(cnt):
|
||||
x1 = min(max(self.mock_x_range[0], random.uniform(x_range[0], x_range[1])), self.mock_x_range[1])
|
||||
y1 = min(max(self.mock_y_range[0], random.uniform(y_range[0], y_range[1])), self.mock_y_range[1])
|
||||
yaw1 = min(max(self.mock_yaw_range[0], random.uniform(yaw_range[0], yaw_range[1])), self.mock_yaw_range[1])
|
||||
pose = [x1, y1, tcp_hover_pose[2], tcp_hover_pose[3], tcp_hover_pose[4], yaw1]
|
||||
self.arm.set_position(*pose, speed=grasp_tcp_speed if i == 0 else spd, wait=True if i == cnt - 1 else False)
|
||||
|
||||
x_range[0] = x1 if x1 < x0 else x0 if x1 == x0 else x_range[0]
|
||||
x_range[1] = x1 if x1 > x0 else x0 if x1 == x0 else x_range[1]
|
||||
y_range[0] = y1 if y1 < y0 else y0 if y1 == y0 else y_range[0]
|
||||
y_range[1] = y1 if y1 > y0 else y0 if y1 == y0 else y_range[1]
|
||||
yaw_range[0] = yaw1 if yaw1 < yaw0 else yaw0 if yaw1 == yaw0 else yaw_range[0]
|
||||
yaw_range[1] = yaw1 if yaw1 > yaw0 else yaw0 if yaw1 == yaw0 else yaw_range[1]
|
||||
elif case_type == 3:
|
||||
# 方案3: 在目标点位上方随机高度一定范围内游走
|
||||
cnt = random.randint(2, 5)
|
||||
for i in range(cnt):
|
||||
x1 = min(max(self.mock_x_range[0], random.uniform(x_range[0], x_range[1])), self.mock_x_range[1])
|
||||
y1 = min(max(self.mock_y_range[0], random.uniform(y_range[0], y_range[1])), self.mock_y_range[1])
|
||||
yaw1 = min(max(self.mock_yaw_range[0], random.uniform(yaw_range[0], yaw_range[1])), self.mock_yaw_range[1])
|
||||
z1 = tcp_hover_pose[2] - (random.uniform(0, self.config.hover_offset - 50) if i != 0 else 0)
|
||||
pose = [x1, y1, z1, tcp_hover_pose[3], tcp_hover_pose[4], yaw]
|
||||
self.arm.set_position(*pose, speed=grasp_tcp_speed if i == 0 else spd, wait=True)
|
||||
if tcp_hover_pose[2] - z1 > 30 or i == cnt - 1:
|
||||
pose = [x1, y1, tcp_hover_pose[2], tcp_hover_pose[3], tcp_hover_pose[4], yaw1]
|
||||
self.arm.set_position(*pose, speed=grasp_tcp_speed if i == 0 else spd, wait=True)
|
||||
|
||||
x_range[0] = x1 if x1 < x0 else x0 if x1 == x0 else x_range[0]
|
||||
x_range[1] = x1 if x1 > x0 else x0 if x1 == x0 else x_range[1]
|
||||
y_range[0] = y1 if y1 < y0 else y0 if y1 == y0 else y_range[0]
|
||||
y_range[1] = y1 if y1 > y0 else y0 if y1 == y0 else y_range[1]
|
||||
yaw_range[0] = yaw1 if yaw1 < yaw0 else yaw0 if yaw1 == yaw0 else yaw_range[0]
|
||||
yaw_range[1] = yaw1 if yaw1 > yaw0 else yaw0 if yaw1 == yaw0 else yaw_range[1]
|
||||
|
||||
print(f'[MOCK] mock_type={self.config.mock_type}, case_type={case_type}, cnt={cnt}')
|
||||
|
||||
# 去到抓取目标正上方
|
||||
code = self.arm.set_position(*tcp_hover_pose, speed=grasp_tcp_speed, wait=True)
|
||||
if code or (events and events['exit_early']): # exit early
|
||||
self.__reset_action_data()
|
||||
return False
|
||||
time.sleep(0.5)
|
||||
|
||||
# 下移到抓取目标位置
|
||||
code = self.arm.set_position(*tcp_target_pose, speed=grasp_tcp_speed, wait=True)
|
||||
if code or (events and events['exit_early']): # exit early
|
||||
self.__reset_action_data()
|
||||
return False
|
||||
time.sleep(0.25)
|
||||
|
||||
if self.config.gripper_type > 0:
|
||||
# 抓取
|
||||
self.arm.set_gripper_position(0, wait=True)
|
||||
time.sleep(0.25)
|
||||
self.__set_record_status(RECORD_NONE)
|
||||
# 记录抓取时的夹爪位置
|
||||
_, gripper_pos = self.arm.get_gripper_position()
|
||||
# 松开(不真正抓取)
|
||||
self.arm.set_gripper_position(800, wait=True)
|
||||
|
||||
self.__set_record_status(RECORD_NO_GRIPPER)
|
||||
# 回到抓取目标正上方
|
||||
code = self.arm.set_position(*tcp_hover_pose, speed=grasp_tcp_speed, wait=True)
|
||||
if code or (events and events['exit_early']): # exit early
|
||||
self.__reset_action_data()
|
||||
return False
|
||||
if self.config.gripper_type > 0:
|
||||
# 把机械爪恢复成抓取时位置
|
||||
self.arm.set_gripper_position(gripper_pos)
|
||||
time.sleep(0.25)
|
||||
|
||||
# 去放置位置正上方
|
||||
place_tcp_hover_pose = self.config.place_tcp_pose.copy()
|
||||
place_tcp_hover_pose[2] += self.config.hover_offset
|
||||
code = self.arm.set_position(*place_tcp_hover_pose, speed=grasp_tcp_speed, wait=True)
|
||||
if code or (events and events['exit_early']): # exit early
|
||||
self.__reset_action_data()
|
||||
return False
|
||||
time.sleep(0.25)
|
||||
|
||||
# 下移到放置位置
|
||||
place_tcp_target_pose = self.config.place_tcp_pose
|
||||
code = self.arm.set_position(*place_tcp_target_pose, speed=grasp_tcp_speed, wait=True)
|
||||
if code or (events and events['exit_early']): # exit early
|
||||
self.__reset_action_data()
|
||||
return False
|
||||
time.sleep(0.25)
|
||||
|
||||
self.__set_record_status(RECORD_POSE_GRIPPER)
|
||||
# 放下
|
||||
if self.config.gripper_type > 0:
|
||||
self.arm.set_gripper_position(800, wait=True)
|
||||
time.sleep(0.25)
|
||||
# 回到放置位置正上方
|
||||
code = self.arm.set_position(*place_tcp_hover_pose, speed=grasp_tcp_speed, wait=True)
|
||||
if code or (events and events['exit_early']): # exit early
|
||||
self.__reset_action_data()
|
||||
return False
|
||||
time.sleep(0.25)
|
||||
# 回到初始位置
|
||||
code = self.arm.set_position(*initial_tcp_pose, speed=grasp_tcp_speed, wait=True)
|
||||
if code or (events and events['exit_early']): # exit early
|
||||
self.__reset_action_data()
|
||||
return False
|
||||
time.sleep(0.25)
|
||||
self.__set_record_status(RECORD_NONE)
|
||||
|
||||
print(f'[MOCK] Recorded {len(self._action_datas)} steps of action data.')
|
||||
|
||||
self.arm.set_mode(6)
|
||||
self.arm.set_state(0)
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
def action_features(self) -> dict:
|
||||
if self._is_joint_space:
|
||||
# Add one more dof for gripper
|
||||
return { f"J{i+1}.pos": float for i in range(7) } | {"gripper.pos": float}
|
||||
else:
|
||||
if self.config.gripper_type > 0:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (7,),
|
||||
"names": {"pose.x": 0, "pose.y": 1, "pose.z": 2, "pose.rx": 3, "pose.ry": 4, "pose.rz": 5, "gripper.pos": 6},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (6,),
|
||||
"names": {"pose.x": 0, "pose.y": 1, "pose.z": 2, "pose.rx": 3, "pose.ry": 4, "pose.rz": 5},
|
||||
}
|
||||
|
||||
@property
|
||||
def feedback_features(self) -> dict:
|
||||
if self._is_joint_space:
|
||||
# Add one more dof for gripper
|
||||
return { f"J{i+1}.pos": float for i in range(7) } | {"gripper.pos": float}
|
||||
else:
|
||||
if self.config.gripper_type > 0:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (7,),
|
||||
"names": {"pose.x": 0, "pose.y": 1, "pose.z": 2, "pose.rx": 3, "pose.ry": 4, "pose.rz": 5, "gripper.pos": 6},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (6,),
|
||||
"names": {"pose.x": 0, "pose.y": 1, "pose.z": 2, "pose.rx": 3, "pose.ry": 4, "pose.rz": 5},
|
||||
}
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self._is_connected
|
||||
|
||||
@property
|
||||
def is_calibrated(self) -> bool:
|
||||
return self._is_calibrated
|
||||
|
||||
def calibrate(self) -> None:
|
||||
# CHECK!!
|
||||
pass
|
||||
|
||||
def configure(self, events=None) -> None:
|
||||
return self.__mock(events=events)
|
||||
|
||||
def connect(self, calibrate: bool = False) -> None:
|
||||
self.__init_robot()
|
||||
self.start()
|
||||
|
||||
def disconnect(self):
|
||||
self._is_connected = False
|
||||
self.arm.set_state(4)
|
||||
self.arm.disconnect()
|
||||
self.join()
|
||||
|
||||
def run(self):
|
||||
sleep_time_s = 1 / self.config.fps
|
||||
next_record_time = time.perf_counter() + sleep_time_s
|
||||
|
||||
buffer = self.rt_sock.recv(4)
|
||||
while len(buffer) < 4:
|
||||
buffer += self.rt_sock.recv(4 - len(buffer))
|
||||
size = convert.bytes_to_u32(buffer[:4])
|
||||
print(f"UFACTORY Robot RT Report Thread starts!! size={size}")
|
||||
gripper_pos = 1.0
|
||||
while self.is_connected:
|
||||
buffer += self.rt_sock.recv(size - len(buffer))
|
||||
if len(buffer) < size:
|
||||
continue
|
||||
data = buffer[:size]
|
||||
buffer = buffer[size:]
|
||||
|
||||
with self._update_lock:
|
||||
if self._record_status:
|
||||
time_now = time.perf_counter()
|
||||
if time_now - next_record_time >= -0.001:
|
||||
if self._is_joint_space:
|
||||
pose = convert.bytes_to_fp32s(data[116:144], 7) # joint angles
|
||||
else:
|
||||
pose = convert.bytes_to_fp32s(data[472:496], 6) # tcp pose
|
||||
|
||||
if self.config.gripper_type > 0:
|
||||
if self._record_status & RECORD_GRIPPER:
|
||||
if size >= 744 and self._report_gripper:
|
||||
external_device_info = convert.bytes_to_16s(data[738:744], 3) # gripper: [pos, speed, current]
|
||||
grippos = min(external_device_info[0] * 10, self.config.gripper_open)
|
||||
else:
|
||||
_, grippos = self.arm.get_gripper_position()
|
||||
grippos = min(grippos, self.config.gripper_open)
|
||||
grippos_norm = (self.config.gripper_open - grippos) / (self.config.gripper_open - self.config.gripper_close)
|
||||
pose.append(grippos_norm) # add gripper pos
|
||||
gripper_pos = grippos_norm
|
||||
else:
|
||||
pose.append(gripper_pos)
|
||||
else:
|
||||
if self._is_joint_space:
|
||||
pose.append(gripper_pos)
|
||||
|
||||
self._action_datas.append(pose)
|
||||
next_record_time = time_now + sleep_time_s
|
||||
|
||||
# delta action
|
||||
def get_action(self) -> dict[str, Any]:
|
||||
if not self.is_connected:
|
||||
raise DeviceNotConnectedError(
|
||||
"UFMockTeleop is not connected. You need to run `connect()` before `get_action()`."
|
||||
)
|
||||
|
||||
if not self._action_datas:
|
||||
if self._is_joint_space:
|
||||
_, pose = self.arm.get_servo_angle(is_radian=True)
|
||||
if self.config.gripper_type > 0:
|
||||
_, grippos = self.arm.get_gripper_position()
|
||||
grippos = min(grippos, self.config.gripper_open)
|
||||
grippos_norm = (self.config.gripper_open - grippos) / (self.config.gripper_open - self.config.gripper_close)
|
||||
pose.append(grippos_norm)
|
||||
else:
|
||||
pose.append(1.0) # gripper pos
|
||||
else:
|
||||
_, pose = self.arm.get_position_aa(is_radian=True)
|
||||
if self.config.gripper_type > 0:
|
||||
_, grippos = self.arm.get_gripper_position()
|
||||
grippos = min(grippos, self.config.gripper_open)
|
||||
grippos_norm = (self.config.gripper_open - grippos) / (self.config.gripper_open - self.config.gripper_close)
|
||||
pose.append(grippos_norm)
|
||||
else:
|
||||
if self._action_inx >= len(self._action_datas):
|
||||
pose = self._action_datas[-1]
|
||||
else:
|
||||
pose = self._action_datas[self._action_inx]
|
||||
self._action_inx += 1
|
||||
action = {}
|
||||
if self._is_joint_space:
|
||||
for i in range(self.arm.axis):
|
||||
action.update({f"J{i+1}.pos": pose[i]})
|
||||
action.update({"gripper.pos": pose[7]})
|
||||
else:
|
||||
action.update({
|
||||
"pose.x": pose[0],
|
||||
"pose.y": pose[1],
|
||||
"pose.z": pose[2],
|
||||
"pose.rx": pose[3],
|
||||
"pose.ry": pose[4],
|
||||
"pose.rz": pose[5],
|
||||
})
|
||||
if self.config.gripper_type > 0:
|
||||
action.update({"gripper.pos": pose[6]})
|
||||
return action
|
||||
|
||||
def send_feedback(self, feedback: dict[str, float]) -> None:
|
||||
raise NotImplementedError
|
||||
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2025 UFACTORY Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Tuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
from lerobot.teleoperators import TeleoperatorConfig
|
||||
|
||||
|
||||
@TeleoperatorConfig.register_subclass("uf::mock_teleop")
|
||||
@dataclass
|
||||
class UFMockTeleopConfig(TeleoperatorConfig):
|
||||
robot_ip: str = "192.168.1.127"
|
||||
control_space: str = "joint" # joint/cartesian
|
||||
mock_type: int = 1
|
||||
mock_x_range: Tuple[float, ...] = (180, 600) # mm
|
||||
mock_y_range: Tuple[float, ...] = (-280, 50) # mm
|
||||
mock_z_range: Tuple[float, ...] = (180, 180) # mm
|
||||
mock_roll_range: Tuple[float, ...] = (180, 180) # °
|
||||
mock_pitch_range: Tuple[float, ...] = (0, 0) # °
|
||||
mock_yaw_range: Tuple[float, ...] = (-90, 90) # °
|
||||
mock_tcp_speed: int = 200 # mm/s
|
||||
hover_offset: int = 200 # mm
|
||||
grasp_tcp_speed: int = 150 # mm/s
|
||||
|
||||
place_tcp_speed: int = 100 # mm/s
|
||||
place_tcp_pose: Tuple[float, ...] = (383, 253, 320, 180, 0, 0) # x,y,z in mm; roll,pitch,yaw in °
|
||||
initial_tcp_pose: Tuple[float, ...] = (450, 0, 520, 180, 0, 0, 0) # x,y,z in mm; roll,pitch,yaw in °
|
||||
|
||||
fps: int = 30 # Hz
|
||||
|
||||
gripper_type: int = 1 # 0: no gripper, 1: xArm Gripper, 2: xArm Gripper G2
|
||||
gripper_freq: int = 50 # Hz
|
||||
gripper_open: int = 800
|
||||
gripper_close: int = 0
|
||||
6
ufactory_lerobot/teleoperators/umi_teleop/__init__.py
Normal file
6
ufactory_lerobot/teleoperators/umi_teleop/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from .umi_teleop_config import UmiTeleopConfig
|
||||
from .umi_teleop import UmiTeleop
|
||||
from .multiple_umi_teleop_config import MultipleUmiTeleopConfig
|
||||
from .multiple_umi_teleop import MultipleUmiTeleop
|
||||
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from typing import Any
|
||||
from .multiple_umi_teleop_config import MultipleUmiTeleopConfig
|
||||
from ..base_teleop import UFBaseTeleop
|
||||
from .umi_teleop import UmiTeleop
|
||||
|
||||
|
||||
class MultipleUmiTeleop(UFBaseTeleop):
|
||||
|
||||
config_class = MultipleUmiTeleopConfig
|
||||
name = "Multiple UMI Teleop For xArm"
|
||||
|
||||
def __init__(self, config: MultipleUmiTeleopConfig):
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
self.teleops = {}
|
||||
for key, teleop_config in self.config.teleops.items():
|
||||
self.teleops[key] = UmiTeleop(teleop_config, prefix=key)
|
||||
|
||||
def action_features(self) -> dict:
|
||||
action_features = {}
|
||||
for teleop in self.teleops.values():
|
||||
action_features.update(teleop.action_features)
|
||||
return action_features
|
||||
|
||||
@property
|
||||
def feedback_features(self) -> dict:
|
||||
feedback_features = {}
|
||||
for teleop in self.teleops.values():
|
||||
feedback_features.update(teleop.feedback_features)
|
||||
return feedback_features
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return all(teleop.is_connected for teleop in self.teleops.values())
|
||||
|
||||
@property
|
||||
def is_calibrated(self) -> bool:
|
||||
return all(teleop.is_calibrated for teleop in self.teleops.values())
|
||||
|
||||
def connect(self, calibrate: bool = True) -> None:
|
||||
for teleop in self.teleops.values():
|
||||
teleop.connect(calibrate=calibrate)
|
||||
|
||||
def calibrate(self) -> None:
|
||||
for teleop in self.teleops.values():
|
||||
teleop.calibrate()
|
||||
|
||||
def configure(self) -> None:
|
||||
for teleop in self.teleops.values():
|
||||
teleop.configure()
|
||||
|
||||
def disconnect(self) -> None:
|
||||
for teleop in self.teleops.values():
|
||||
teleop.disconnect()
|
||||
|
||||
def set_ctrl_status(self, status):
|
||||
for teleop in self.teleops.values():
|
||||
teleop.set_ctrl_status(status)
|
||||
|
||||
def get_action(self) -> dict[str, Any]:
|
||||
actions = {}
|
||||
for teleop in self.teleops.values():
|
||||
actions.update(teleop.get_action())
|
||||
return actions
|
||||
|
||||
def send_feedback(self, feedback: dict[str, Any]) -> None:
|
||||
for key, teleop in self.teleops.items():
|
||||
feedback_subset = {k: v for k, v in feedback.items() if k.startswith(f"{key}.")}
|
||||
teleop.send_feedback(feedback_subset)
|
||||
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2025 UFACTORY Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from lerobot.teleoperators import TeleoperatorConfig
|
||||
from .umi_teleop_config import UmiTeleopConfig
|
||||
|
||||
|
||||
@TeleoperatorConfig.register_subclass("uf::multiple_umi_teleop")
|
||||
@dataclass
|
||||
class MultipleUmiTeleopConfig(TeleoperatorConfig):
|
||||
teleops: dict[str, UmiTeleopConfig]
|
||||
201
ufactory_lerobot/teleoperators/umi_teleop/umi_teleop.py
Normal file
201
ufactory_lerobot/teleoperators/umi_teleop/umi_teleop.py
Normal file
@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
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 ufactory_lerobot.devices.umi.xvlib import XVLib
|
||||
from ..base_teleop import UFBaseTeleop
|
||||
from .umi_teleop_config import UmiTeleopConfig
|
||||
|
||||
|
||||
class UmiTeleop(UFBaseTeleop):
|
||||
|
||||
config_class = UmiTeleopConfig
|
||||
name = "UMI Teleop For xArm"
|
||||
|
||||
def __init__(self, config: UmiTeleopConfig, prefix=''):
|
||||
super().__init__(config)
|
||||
self.config = config
|
||||
self.prefix = '' if not prefix else f'{prefix}.'
|
||||
self._is_connected = False
|
||||
self._is_calibrated = True
|
||||
|
||||
if self.config.use_gripper:
|
||||
self.config.init_clamp_stream = True
|
||||
else:
|
||||
self.config.init_clamp_stream = False
|
||||
|
||||
self.tracker = ViveTracker() if self.config.use_vive_tracker else None
|
||||
self.xvlib = XVLib(self.config.serial_number, self.config.init_slam, self.config.init_clamp_stream, self.config.init_color_camera, self.config.init_fisheye_cameras)
|
||||
|
||||
# tracker_to_robot_eef = [0, 0, 0, math.pi / 2, -math.pi / 2, 0]
|
||||
# tracker_to_robot_eef = [0, 0, 0, 0, 0, -math.pi/2]
|
||||
# tracker_to_robot_eef = [0, 0, 0, math.pi, math.pi, 0] # Test1
|
||||
# tracker_to_robot_eef = [0, 0, 0, math.pi, math.pi, -math.pi/2] # Dual left
|
||||
# tracker_to_robot_eef = [0, 0, 0, math.pi, math.pi, math.pi/2] # Dual right
|
||||
tracker_to_robot_eef = self.config.tracker_to_robot_eef
|
||||
self.tracker_to_robot_matrix = Transformations.xyzrpy_to_rotation_matrix(*tracker_to_robot_eef)
|
||||
# robot_base_pose = [300, 0, 300, 0, 0, 0]
|
||||
# robot_base_pose = [300, 0, 300, math.pi, -math.pi/2, 0]
|
||||
# robot_base_pose = [220, 0, 385, math.pi, 0, 0]
|
||||
# robot_base_pose = [250, 0, 150, math.pi, 0, 0] # Test 1
|
||||
# robot_base_pose = [250, 0, 150, math.pi, 0, math.pi/2] # Dual left
|
||||
# robot_base_pose = [250, 0, 150, math.pi, 0, math.pi/2] # Dual right
|
||||
robot_base_pose = self.config.robot_base_pose
|
||||
self.robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*robot_base_pose)
|
||||
self.begin_tracker_robot_matrix = None
|
||||
|
||||
@property
|
||||
def action_features(self) -> dict:
|
||||
if self.config.use_gripper:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (7,),
|
||||
"names": {f"{self.prefix}pose.x": 0, f"{self.prefix}pose.y": 1, f"{self.prefix}pose.z": 2, f"{self.prefix}pose.rx": 3, f"{self.prefix}pose.ry": 4, f"{self.prefix}pose.rz": 5, f"{self.prefix}gripper.pos": 6},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (6,),
|
||||
"names": {f"{self.prefix}pose.x": 0, f"{self.prefix}pose.y": 1, f"{self.prefix}pose.z": 2, f"{self.prefix}pose.rx": 3, f"{self.prefix}pose.ry": 4, f"{self.prefix}pose.rz": 5},
|
||||
}
|
||||
|
||||
@property
|
||||
def feedback_features(self) -> dict:
|
||||
if self.config.use_gripper:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (7,),
|
||||
"names": {f"{self.prefix}pose.x": 0, f"{self.prefix}pose.y": 1, f"{self.prefix}pose.z": 2, f"{self.prefix}pose.rx": 3, f"{self.prefix}pose.ry": 4, f"{self.prefix}pose.rz": 5, f"{self.prefix}gripper.pos": 6},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"dtype": "float32",
|
||||
"shape": (6,),
|
||||
"names": {f"{self.prefix}pose.x": 0, f"{self.prefix}pose.y": 1, f"{self.prefix}pose.z": 2, f"{self.prefix}pose.rx": 3, f"{self.prefix}pose.ry": 4, f"{self.prefix}pose.rz": 5},
|
||||
}
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
return self._is_connected
|
||||
|
||||
@property
|
||||
def is_calibrated(self) -> bool:
|
||||
return self._is_calibrated
|
||||
|
||||
def calibrate(self) -> None:
|
||||
# CHECK!!
|
||||
pass
|
||||
|
||||
def configure(self) -> None:
|
||||
pass
|
||||
|
||||
def connect(self, calibrate: bool = False) -> None:
|
||||
self.xvlib.xv_init(self.config.serial_number, self.config.init_slam, self.config.init_clamp_stream, self.config.init_color_camera, self.config.init_fisheye_cameras)
|
||||
self._is_connected = True
|
||||
|
||||
def disconnect(self):
|
||||
self.xvlib.xv_uninit()
|
||||
self._is_connected = False
|
||||
|
||||
@staticmethod
|
||||
def normalize_angle(angle_deg):
|
||||
"""
|
||||
将角度归一化到 [-180, 180] 区间
|
||||
"""
|
||||
while angle_deg > 180:
|
||||
angle_deg -= 360
|
||||
while angle_deg < -180:
|
||||
angle_deg += 360
|
||||
return angle_deg
|
||||
|
||||
def set_ctrl_status(self, status):
|
||||
if status:
|
||||
self.begin_tracker_robot_matrix = None
|
||||
else:
|
||||
pass
|
||||
|
||||
# delta action
|
||||
def get_action(self) -> dict[str, Any]:
|
||||
if not self.is_connected:
|
||||
raise DeviceNotConnectedError(
|
||||
"UmiTeleop is not connected. You need to run `connect()` before `get_action()`."
|
||||
)
|
||||
|
||||
if self.tracker is not None:
|
||||
pose_data = self.tracker.get_pose(self.config.vive_tracker_id)
|
||||
if pose_data is None:
|
||||
print('cant not get pose from vive tracker')
|
||||
_, pose_data = self.xvlib.xv_get_slam_data()
|
||||
else:
|
||||
_, pose_data = self.xvlib.xv_get_slam_data()
|
||||
position = pose_data.position.to_list(6)
|
||||
quaternion = pose_data.quaternion.to_list(6)
|
||||
# orientation = pose_data.orientation.to_list()
|
||||
|
||||
# x, y, z = position[0] * 1000, position[1] * 1000, position[2] * 1000
|
||||
# roll, pitch, yaw = math.degrees(orientation[0]), math.degrees(orientation[1]), math.degrees(orientation[2])
|
||||
# print(f'[1] x={x:.1f}, y={y:.1f}, z={z:.1f}, roll={roll:.1f}, pitch={pitch:.1f}, yaw={yaw:.1f}')
|
||||
|
||||
# x, y, z = position[2] * 1000, position[0] * 1000, position[1] * 1000
|
||||
# roll, pitch, yaw = orientation[0], orientation[1], orientation[2]
|
||||
# roll, pitch, yaw = math.degrees(roll), math.degrees(pitch), math.degrees(yaw)
|
||||
# print(f'[1] x={x:.1f}, y={y:.1f}, z={z:.1f}, roll={roll:.1f}, pitch={pitch:.1f}, yaw={yaw:.1f}')
|
||||
|
||||
# x, y, z = position[0] * 1000, position[1] * 1000, position[2] * 1000
|
||||
# R_A = Transformations.quaternion_to_rotation_matrix(quaternion)
|
||||
# roll, pitch, yaw = Transformations.rotation_matrix_to_rpy(R_A)
|
||||
# roll, pitch, yaw = math.degrees(roll), math.degrees(pitch), math.degrees(yaw)
|
||||
# print(f'[2] x={x:.1f}, y={y:.1f}, z={z:.1f}, roll={roll:.1f}, pitch={pitch:.1f}, yaw={yaw:.1f}')
|
||||
# print('*' * 50)
|
||||
# roll, pitch, yaw = math.degrees(orientation[0]), math.degrees(orientation[1]), math.degrees(orientation[2])
|
||||
# print(f'[1] x={x:.1f}, y={y:.1f}, z={z:.1f}, roll={roll:.1f}, pitch={pitch:.1f}, yaw={yaw:.1f}')
|
||||
|
||||
x, y, z = position[0] * 1000, position[1] * 1000, position[2] * 1000
|
||||
tracker_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, quaternion, self.tracker_to_robot_matrix)
|
||||
if self.begin_tracker_robot_matrix is None:
|
||||
self.begin_tracker_robot_matrix = tracker_robot_matrix
|
||||
|
||||
robot_target_pose = Transformations.tracker_robot_matrix_to_robot_pose(self.begin_tracker_robot_matrix, tracker_robot_matrix, self.robot_base_matrix, is_axis_angle=True)
|
||||
x, y, z = robot_target_pose[0:3]
|
||||
orientation = robot_target_pose[3:6]
|
||||
# roll, pitch, yaw = list(map(math.degrees, orientation))
|
||||
# print(f'[{self.config.serial_number}] x={x:.3f}, y={y:.3f}, z={z:.3f}, rx={roll:.3f}, ry={pitch:.3f}, rz={yaw:.3f}')
|
||||
|
||||
# R_prev = Transformations.rpy_to_rotation_matrix(math.pi, -math.pi / 2, 0)
|
||||
# R_delta = Transformations.rxryrz_to_matrix(robot_target_pose[3:6])
|
||||
# R_curr = R_prev @ R_delta
|
||||
# # # R_curr = R_prev.apply(R_delta)
|
||||
# orientation = Transformations.rotation_matrix_to_rxryrz(R_curr)
|
||||
|
||||
# roll, pitch, yaw = math.degrees(orientation[0]), math.degrees(orientation[1]), math.degrees(orientation[2])
|
||||
# print(f'[2] x={x:.1f}, y={y:.1f}, z={z:.1f}, rx={roll:.1f}, ry={pitch:.1f}, rz={yaw:.1f}')
|
||||
# print('*' * 50)
|
||||
|
||||
# output is delta change of the robot pose
|
||||
action_dict = {
|
||||
# "pose.x": z,
|
||||
# "pose.y": -y,
|
||||
# "pose.z": x,
|
||||
# "pose.rx": orientation[2],
|
||||
# "pose.ry": -orientation[1],
|
||||
# "pose.rz": orientation[0],
|
||||
f"{self.prefix}pose.x": x,
|
||||
f"{self.prefix}pose.y": y,
|
||||
f"{self.prefix}pose.z": z,
|
||||
f"{self.prefix}pose.rx": orientation[0],
|
||||
f"{self.prefix}pose.ry": orientation[1],
|
||||
f"{self.prefix}pose.rz": orientation[2],
|
||||
}
|
||||
|
||||
if self.config.use_gripper:
|
||||
_, clamp_data = self.xvlib.xv_get_clamp_stream_data()
|
||||
gripper_pos = (87 - clamp_data.data) / (87 - 0)
|
||||
action_dict.update({f"{self.prefix}gripper.pos": gripper_pos})
|
||||
|
||||
return action_dict
|
||||
|
||||
def send_feedback(self, feedback: dict[str, float]) -> None:
|
||||
raise NotImplementedError
|
||||
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2025 UFACTORY Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple
|
||||
import numpy as np
|
||||
from lerobot.teleoperators import TeleoperatorConfig
|
||||
|
||||
|
||||
@TeleoperatorConfig.register_subclass("uf::umi_teleop")
|
||||
@dataclass
|
||||
class UmiTeleopConfig(TeleoperatorConfig):
|
||||
serial_number: str
|
||||
init_slam: bool = True
|
||||
init_clamp_stream: bool = True
|
||||
init_color_camera: bool = False
|
||||
init_fisheye_cameras: bool = False
|
||||
use_gripper: bool = True
|
||||
use_vive_tracker: bool = False
|
||||
vive_tracker_id: str = 'WM0'
|
||||
tracker_to_robot_eef: Tuple[float, ...] = (0, 0, 0, 0, 0, -np.pi/2)
|
||||
robot_base_pose: Tuple[float, ...] = (300, 0, 300, np.pi, -np.pi/2, 0)
|
||||
40
ufactory_lerobot/teleoperators/utils.py
Normal file
40
ufactory_lerobot/teleoperators/utils.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from lerobot.teleoperators.utils import make_teleoperator_from_config as lerobot_make_teleoperator_from_config
|
||||
from lerobot.teleoperators.config import TeleoperatorConfig
|
||||
from lerobot.teleoperators.teleoperator import Teleoperator
|
||||
|
||||
|
||||
def make_teleoperator_from_config(config: TeleoperatorConfig) -> Teleoperator:
|
||||
if config.type == "uf::gello_teleop":
|
||||
from .gello_teleop import GelloTeleop
|
||||
return GelloTeleop(config)
|
||||
elif config.type == "uf::pika_teleop":
|
||||
from .pika_teleop import PikaTeleop
|
||||
return PikaTeleop(config)
|
||||
elif config.type == "uf::spacemouse_teleop":
|
||||
from .space_mouse import SpaceMouseTeleop
|
||||
return SpaceMouseTeleop(config)
|
||||
elif config.type == "uf::mock_teleop":
|
||||
from .uf_mock_teleop import UFMockTeleop
|
||||
return UFMockTeleop(config)
|
||||
elif config.type == "uf::umi_teleop":
|
||||
from .umi_teleop import UmiTeleop
|
||||
return UmiTeleop(config)
|
||||
elif config.type == "uf::multiple_umi_teleop":
|
||||
from .umi_teleop import MultipleUmiTeleop
|
||||
return MultipleUmiTeleop(config)
|
||||
else:
|
||||
return lerobot_make_teleoperator_from_config(config)
|
||||
1
ufactory_lerobot/utils/__init__.py
Normal file
1
ufactory_lerobot/utils/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .utils import instantiate_from_dict
|
||||
18
ufactory_lerobot/utils/utils.py
Normal file
18
ufactory_lerobot/utils/utils.py
Normal file
@ -0,0 +1,18 @@
|
||||
import importlib
|
||||
|
||||
# recursive call, inspired from gello_software:
|
||||
def instantiate_from_dict(cfg, ignore_cameras=False):
|
||||
"""Instantiate objects from configuration."""
|
||||
if isinstance(cfg, dict) and "_target_" in cfg:
|
||||
module_path, class_name = cfg["_target_"].rsplit(".", 1)
|
||||
cls = getattr(importlib.import_module(module_path), class_name)
|
||||
kwargs = {k: v for k, v in cfg.items() if k != "_target_"}
|
||||
# pp_dict ={k: instantiate_from_dict(v, ignore_cameras) for k, v in kwargs.items()}
|
||||
# print(pp_dict)
|
||||
return cls(**{k: {} if ignore_cameras and k == 'cameras' else instantiate_from_dict(v, ignore_cameras) for k, v in kwargs.items()})
|
||||
elif isinstance(cfg, dict):
|
||||
return {k: {} if ignore_cameras and k == 'cameras' else instantiate_from_dict(v, ignore_cameras) for k, v in cfg.items()}
|
||||
elif isinstance(cfg, list):
|
||||
return [instantiate_from_dict(v, ignore_cameras) for v in cfg]
|
||||
else:
|
||||
return cfg
|
||||
Loading…
Reference in New Issue
Block a user