当前位置: 首页 > backend >正文

宇树 G1 部署(九)——遥操作控制脚本 teleop_hand_and_arm.py 分析与测试部署

首先,我使用的是 v1.0 版本,宇树最近发力了更新的很快:xr_teleoperate-1.0

teleop_hand_and_arm.py 支持通过 XR 设备(比如手势或手柄)来控制实际机器人动作,也支持在虚拟仿真中运行。可以根据需要,通过命令行参数来配置运行方式

启动参数说明:

 1. 基础控制参数

⚙️ 参数📜 说明🔘 目前可选值📌 默认值
--xr-mode选择 XR 输入模式(通过什么方式控制机器人)

hand(手势跟踪)

controller(手柄跟踪)

hand
--arm选择机器人设备类型

G1_29

G1_23

H1_2 H1

G1_29
--ee选择手臂的末端执行器设备类型

dex1

dex3

inspire1

无默认值

 2. 模式开关参数

⚙️ 参数📜 说明
--record【启用数据录制模式】
按 r 键进入遥操后,按 s 键可开启数据录制,再次按 s 键可结束录制并保存本次 episode 数据
继续按下 s 键可重复前述过程
--motion【启用运动控制模式】
开启本模式后,可在机器人运控程序运行下进行遥操作程序
手势跟踪模式下,可使用 R3遥控器 控制机器人正常行走;手柄跟踪模式下,也可使用手柄摇杆控制机器人行走
--headless【启用无图形界面模式】
适用于本程序部署在开发计算单元(PC2)等无显示器情况
--sim【启用仿真模式】

选择手势跟踪来控制 G1(29 DoF) + inspire1 灵巧手设备,同时开启数据录制模式,则启动命令如下所示(建议在 terminal 中运行):

python teleop_hand_and_arm.py --xr-mode=hand --arm=G1_29 --ee=inspire1 --record --motion

接下来详细分析一下这个脚本,这个脚本高度模块化,可扩展性极好,并且采用并发设计,可以做很多二开

目录

1 脚本解析

1.1 导入依赖及路径配置

1.2 仿真控制辅助函数

1.3 全局状态管理与键盘监听回调

1.4 主函数入口与参数解析

1.5 图像通道配置与共享内存初始化

1.6 XR/手势与机器人运动控制绑定

1.7 仿真与运动客户端初始化

1.8 主循环 - 控制、记录与交互核心

1.9 异常处理与资源清理

2 相机适配与脚本测试

2.1 相机适配

2.2 脚本测试

2.1 图像服务

2.2 主控脚本测试


1 脚本解析

1.1 导入依赖及路径配置

import numpy as np  # 科学计算库,主要用于矩阵、图像等数据处理
import time         # 提供时间和延迟函数
import argparse     # 用于命令行参数解析
import cv2          # OpenCV,常用于图像处理和显示
from multiprocessing import shared_memory, Value, Array, Lock  # 多进程数据共享和同步
import threading    # 多线程,用于异步任务如键盘监听、图像接收
import logging_mp   # 自定义多进程日志模块,便于跨进程日志收集
logging_mp.basic_config(level=logging_mp.INFO)  # 设置日志等级为INFO
logger_mp = logging_mp.get_logger(__name__)     # 获取当前模块的日志实例import os           # 操作系统相关
import sys          # 系统参数和函数
current_dir = os.path.dirname(os.path.abspath(__file__))  # 当前脚本所在目录
parent_dir = os.path.dirname(current_dir)                 # 上级目录(项目根目录)
sys.path.append(parent_dir)                               # 把根目录加入模块搜索路径,便于import自定义模块# 导入自定义功能模块
from televuer import TeleVuerWrapper    # XR/VR交互与手势、相机接口
from teleop.robot_control.robot_arm import G1_29_ArmController, G1_23_ArmController, H1_2_ArmController, H1_ArmController   # 手臂控制器
from teleop.robot_control.robot_arm_ik import G1_29_ArmIK, G1_23_ArmIK, H1_2_ArmIK, H1_ArmIK   # 手臂逆解算器
from teleop.robot_control.robot_hand_unitree import Dex3_1_Controller, Gripper_Controller      # Unitree手/夹爪控制器
from teleop.robot_control.robot_hand_inspire import Inspire_Controller                         # Inspire手控制器
from teleop.image_server.image_client import ImageClient    # 远程图像采集客户端
from teleop.utils.episode_writer import EpisodeWriter       # episode数据记录器
from sshkeyboard import listen_keyboard, stop_listening     # 跨平台键盘监听,非阻塞# 仿真相关:Isaac通信话题
from unitree_sdk2py.core.channel import ChannelPublisher    # DDS通信话题发布器
from unitree_sdk2py.idl.std_msgs.msg.dds_ import String_    # 通用字符串消息类型
  • televuer:XR/VR端的手势、头显追踪数据与画面交互桥
  • robot_control:控制不同型号手臂和末端执行器
  • episode_writer:用于实验数据记录和存档
  • ImageClient:用于与远端摄像头画面服务器通信
  • sshkeyboard:跨平台键盘监听

1.2 仿真控制辅助函数

def publish_reset_category(category: int, publisher):       # 发布仿真场景重置信号msg = String_(data=str(category))                      # 构造消息publisher.Write(msg)                                   # 发布logger_mp.info(f"published reset category: {category}")# 日志记录

向 Isaac 仿真环境发布场景重置信号,便于环境回溯、重新初始化

1.3 全局状态管理与键盘监听回调

# 全局运行状态变量
start_signal = False        # 标志是否开始主循环(r键触发)
running = True              # 主循环运行标志
should_toggle_recording = False   # 是否切换录制状态(s键触发)
is_recording = False        # 当前是否处于录制状态def on_press(key):          # 键盘按下回调global running, start_signal, should_toggle_recordingif key == 'r':                  # 'r'键:启动主循环start_signal = Truelogger_mp.info("Program start signal received.")elif key == 'q':                # 'q'键:退出主循环stop_listening()running = Falseelif key == 's':                # 's'键:切换录制should_toggle_recording = Trueelse:logger_mp.info(f"{key} was pressed, but no action is defined for this key.")# 启动键盘监听线程,监听过程中不会阻塞主线程
listen_keyboard_thread = threading.Thread(target=listen_keyboard, kwargs={"on_press": on_press, "until": None, "sequential": False,},daemon=True
)
listen_keyboard_thread.start()  # 启动监听
  • 维护主状态机(是否运行、是否录制等),通过键盘控制流程转移(r:开始;q:退出;s:切换录制)
  • listen_keyboard_thread:监听线程,用于实现用户无阻塞交互

1.4 主函数入口与参数解析

if __name__ == '__main__':   # 程序主入口parser = argparse.ArgumentParser()   # 创建参数解析器parser.add_argument('--task_dir', type=str, default='./utils/data', help='path to save data')  # 数据保存路径parser.add_argument('--frequency', type=float, default=90.0, help="save data's frequency")     # 采样频率parser.add_argument('--xr-mode', type=str, choices=['hand', 'controller'], default='hand', help='Select XR device tracking source') # XR输入模式parser.add_argument('--arm', type=str, choices=['G1_29', 'G1_23', 'H1_2', 'H1'], default='G1_29', help='Select arm controller')      # 选用手臂parser.add_argument('--ee', type=str, choices=['dex3', 'gripper', 'inspire1'], help='Select end effector controller')               # 末端类型parser.add_argument('--record', action='store_true', help='Enable data recording')           # 是否录制数据parser.add_argument('--motion', action='store_true', help='Enable motion control mode')      # 是否开启运动模式parser.add_argument('--headless', action='store_true', help='Enable headless mode (no display)') # 是否无头(无窗口)模式parser.add_argument('--sim', action='store_true', help='Enable isaac simulation mode')       # 是否仿真args = parser.parse_args()         # 解析参数logger_mp.info(f"args: {args}")    # 输出参数配置

命令行参数管理,支持灵活配置,如:数据路径、帧率、手臂类型、末端类型、是否仿真、是否录制、是否无头模式等

1.5 图像通道配置与共享内存初始化

    # -------- 图像客户端/共享内存配置 --------if args.sim:  # 仿真配置img_config = {'fps': 30,'head_camera_type': 'opencv','head_camera_image_shape': [480, 640],    # 头部相机分辨率'head_camera_id_numbers': [0],'wrist_camera_type': 'opencv','wrist_camera_image_shape': [480, 640],   # 腕部相机分辨率'wrist_camera_id_numbers': [2, 4],}else:         # 实机配置img_config = {'fps': 30,'head_camera_type': 'opencv','head_camera_image_shape': [480, 1280],   # 头部相机分辨率(1280更宽,可能双目)'head_camera_id_numbers': [0],'wrist_camera_type': 'opencv','wrist_camera_image_shape': [480, 640],'wrist_camera_id_numbers': [2, 4],}ASPECT_RATIO_THRESHOLD = 2.0  # 判别双目/单目的分辨率阈值# 判断是否为双目头相机if len(img_config['head_camera_id_numbers']) > 1 or (img_config['head_camera_image_shape'][1] / img_config['head_camera_image_shape'][0] > ASPECT_RATIO_THRESHOLD):BINOCULAR = Trueelse:BINOCULAR = False# 是否有腕部相机if 'wrist_camera_type' in img_config:WRIST = Trueelse:WRIST = False# 设置头部相机的共享内存图像shapeif BINOCULAR and not (img_config['head_camera_image_shape'][1] / img_config['head_camera_image_shape'][0] > ASPECT_RATIO_THRESHOLD):tv_img_shape = (img_config['head_camera_image_shape'][0], img_config['head_camera_image_shape'][1] * 2, 3)else:tv_img_shape = (img_config['head_camera_image_shape'][0], img_config['head_camera_image_shape'][1], 3)# 申请头部相机的共享内存,并映射为numpy数组tv_img_shm = shared_memory.SharedMemory(create=True, size=np.prod(tv_img_shape) * np.uint8().itemsize)tv_img_array = np.ndarray(tv_img_shape, dtype=np.uint8, buffer=tv_img_shm.buf)# 腕部相机同理,判断WRIST和仿真,创建对应的共享内存和客户端if WRIST and args.sim:wrist_img_shape = (img_config['wrist_camera_image_shape'][0], img_config['wrist_camera_image_shape'][1] * 2, 3)wrist_img_shm = shared_memory.SharedMemory(create=True, size=np.prod(wrist_img_shape) * np.uint8().itemsize)wrist_img_array = np.ndarray(wrist_img_shape, dtype=np.uint8, buffer=wrist_img_shm.buf)img_client = ImageClient(tv_img_shape=tv_img_shape, tv_img_shm_name=tv_img_shm.name, wrist_img_shape=wrist_img_shape, wrist_img_shm_name=wrist_img_shm.name, server_address="127.0.0.1")elif WRIST and not args.sim:wrist_img_shape = (img_config['wrist_camera_image_shape'][0], img_config['wrist_camera_image_shape'][1] * 2, 3)wrist_img_shm = shared_memory.SharedMemory(create=True, size=np.prod(wrist_img_shape) * np.uint8().itemsize)wrist_img_array = np.ndarray(wrist_img_shape, dtype=np.uint8, buffer=wrist_img_shm.buf)img_client = ImageClient(tv_img_shape=tv_img_shape, tv_img_shm_name=tv_img_shm.name, wrist_img_shape=wrist_img_shape, wrist_img_shm_name=wrist_img_shm.name)else:  # 没有腕部相机,仅头部相机img_client = ImageClient(tv_img_shape=tv_img_shape, tv_img_shm_name=tv_img_shm.name)# 图像接收线程,持续将远程图像写入共享内存image_receive_thread = threading.Thread(target=img_client.receive_process, daemon=True)image_receive_thread.daemon = Trueimage_receive_thread.start()
  • 兼容多相机(头、腕)、多分辨率与双目/单目场景

  • 用共享内存高效实现多进程(如图像采集与主控)之间的数据高速传递,保证零拷贝

  • 图像客户端单独线程异步接收,保证主线程不卡顿

1.6 XR/手势与机器人运动控制绑定

    # XR桥接实例,负责手势/动作/图像数据交互tv_wrapper = TeleVuerWrapper(binocular=BINOCULAR, use_hand_tracking=args.xr_mode == 'hand',img_shape=tv_img_shape, img_shm_name=tv_img_shm.name, return_state_data=True, return_hand_rot_data=False)# -------- 机器人臂、末端控制器与逆运动学模块加载 --------if args.arm == 'G1_29':arm_ctrl = G1_29_ArmController(motion_mode=args.motion, simulation_mode=args.sim)arm_ik = G1_29_ArmIK()elif args.arm == 'G1_23':arm_ctrl = G1_23_ArmController(simulation_mode=args.sim)arm_ik = G1_23_ArmIK()elif args.arm == 'H1_2':arm_ctrl = H1_2_ArmController(simulation_mode=args.sim)arm_ik = H1_2_ArmIK()elif args.arm == 'H1':arm_ctrl = H1_ArmController(simulation_mode=args.sim)arm_ik = H1_ArmIK()# -------- 末端执行器控制器/数据区初始化 --------if args.ee == "dex3":  # 双Dex3机械手left_hand_pos_array = Array('d', 75, lock=True)      # 左手输入 75维right_hand_pos_array = Array('d', 75, lock=True)     # 右手输入 75维dual_hand_data_lock = Lock()                         # 双手输入锁dual_hand_state_array = Array('d', 14, lock=False)   # 输出当前左右手statedual_hand_action_array = Array('d', 14, lock=False)  # 输出当前左右手actionhand_ctrl = Dex3_1_Controller(left_hand_pos_array, right_hand_pos_array, dual_hand_data_lock,dual_hand_state_array, dual_hand_action_array)elif args.ee == "gripper":  # 双夹爪left_gripper_value = Value('d', 0.0, lock=True)      # 左夹爪输入right_gripper_value = Value('d', 0.0, lock=True)     # 右夹爪输入dual_gripper_data_lock = Lock()                      # 夹爪同步锁dual_gripper_state_array = Array('d', 2, lock=False) # 输出当前左右夹爪statedual_gripper_action_array = Array('d', 2, lock=False)# 输出当前左右夹爪actiongripper_ctrl = Gripper_Controller(left_gripper_value, right_gripper_value, dual_gripper_data_lock,dual_gripper_state_array, dual_gripper_action_array, simulation_mode=args.sim)elif args.ee == "inspire1":  # Inspire手left_hand_pos_array = Array('d', 75, lock=True)      # Inspire手输入right_hand_pos_array = Array('d', 75, lock=True)dual_hand_data_lock = Lock()dual_hand_state_array = Array('d', 12, lock=False)dual_hand_action_array = Array('d', 12, lock=False)hand_ctrl = Inspire_Controller(left_hand_pos_array, right_hand_pos_array, dual_hand_data_lock,dual_hand_state_array, dual_hand_action_array)else:pass  # 没选末端则跳过
  • XR 设备(如 Apple Vision)追踪的数据,通过 tv_wrapper 进行抽象和接口化,统一管理
  • 根据参数,灵活加载目标机器人的运动学与执行器控制(如 7/6轴、Dex3、夹爪等)

1.7 仿真与运动客户端初始化

    # -------- 仿真相关初始化 --------if args.sim:reset_pose_publisher = ChannelPublisher("rt/reset_pose/cmd", String_)  # 创建仿真重置话题reset_pose_publisher.Init()                                            # 初始化from teleop.utils.sim_state_topic import start_sim_state_subscribe     # 仿真状态订阅sim_state_subscriber = start_sim_state_subscribe()# -------- 控制器运动模式初始化 --------if args.xr_mode == 'controller' and args.motion:from unitree_sdk2py.g1.loco.g1_loco_client import LocoClientsport_client = LocoClient()sport_client.SetTimeout(0.0001)  # 设置通信超时sport_client.Init()# -------- 数据录制器初始化 --------if args.record and args.headless:recorder = EpisodeWriter(task_dir=args.task_dir, frequency=args.frequency, rerun_log=False)   # 无头模式:不复现elif args.record and not args.headless:recorder = EpisodeWriter(task_dir=args.task_dir, frequency=args.frequency, rerun_log=True)    # GUI模式:可复现
  • 仿真/实机自适应

  • 数据记录支持无头与 GUI 两种模式(适配远程部署/本地实验)

  • LocoClient 用于高层次运动控制

1.8 主循环 - 控制、记录与交互核心

    try:logger_mp.info("Please enter the start signal (enter 'r' to start the subsequent program)")# 等待用户按下 'r' 启动程序while not start_signal:time.sleep(0.01)arm_ctrl.speed_gradual_max()   # 启动时速度梯度最大化,安全启动while running:                 # 主循环,按frequency频率循环start_time = time.time()if not args.headless:      # 非无头模式,显示主画面tv_resized_image = cv2.resize(tv_img_array, (tv_img_shape[1] // 2, tv_img_shape[0] // 2))cv2.imshow("record image", tv_resized_image)key = cv2.waitKey(1) & 0xFFif key == ord('q'):stop_listening()running = Falseif args.sim:publish_reset_category(2, reset_pose_publisher)elif key == ord('s'):should_toggle_recording = Trueelif key == ord('a'):if args.sim:publish_reset_category(2, reset_pose_publisher)if args.record and should_toggle_recording:should_toggle_recording = Falseif not is_recording:if recorder.create_episode():is_recording = Trueelse:logger_mp.error("Failed to create episode. Recording not started.")else:is_recording = Falserecorder.save_episode()if args.sim:publish_reset_category(1, reset_pose_publisher)# 获取XR/VR动作输入tele_data = tv_wrapper.get_motion_state_data()# 不同末端/输入模式下的输入数据赋值if (args.ee == 'dex3' or args.ee == 'inspire1') and args.xr_mode == 'hand':with left_hand_pos_array.get_lock():left_hand_pos_array[:] = tele_data.left_hand_pos.flatten()with right_hand_pos_array.get_lock():right_hand_pos_array[:] = tele_data.right_hand_pos.flatten()elif args.ee == 'gripper' and args.xr_mode == 'controller':with left_gripper_value.get_lock():left_gripper_value.value = tele_data.left_trigger_valuewith right_gripper_value.get_lock():right_gripper_value.value = tele_data.right_trigger_valueelif args.ee == 'gripper' and args.xr_mode == 'hand':with left_gripper_value.get_lock():left_gripper_value.value = tele_data.left_pinch_valuewith right_gripper_value.get_lock():right_gripper_value.value = tele_data.right_pinch_valueelse:pass        # 高级运动控制(如遥控器模式)if args.xr_mode == 'controller' and args.motion:# 右A键退出if tele_data.tele_state.right_aButton:running = False# 左右摇杆共同按下,减震模式(紧急停止)if tele_data.tele_state.left_thumbstick_state and tele_data.tele_state.right_thumbstick_state:sport_client.Damp()# 运动控制,速度限制在0.3以内sport_client.Move(-tele_data.tele_state.left_thumbstick_value[1] * 0.3,-tele_data.tele_state.left_thumbstick_value[0] * 0.3,-tele_data.tele_state.right_thumbstick_value[0] * 0.3)# 获取当前双臂关节状态和速度current_lr_arm_q  = arm_ctrl.get_current_dual_arm_q()current_lr_arm_dq = arm_ctrl.get_current_dual_arm_dq()# 求逆解得到目标关节和力矩time_ik_start = time.time()sol_q, sol_tauff  = arm_ik.solve_ik(tele_data.left_arm_pose, tele_data.right_arm_pose,current_lr_arm_q, current_lr_arm_dq)time_ik_end = time.time()logger_mp.debug(f"ik:\t{round(time_ik_end - time_ik_start, 6)}")# 控制机械臂arm_ctrl.ctrl_dual_arm(sol_q, sol_tauff)# ------ 数据录制流程 ------if args.record:# 各种末端/模式下状态和动作的收集与组织if args.ee == "dex3" and args.xr_mode == 'hand':with dual_hand_data_lock:left_ee_state = dual_hand_state_array[:7]right_ee_state = dual_hand_state_array[-7:]left_hand_action = dual_hand_action_array[:7]right_hand_action = dual_hand_action_array[-7:]current_body_state = []current_body_action = []elif args.ee == "gripper" and args.xr_mode == 'hand':with dual_gripper_data_lock:left_ee_state = [dual_gripper_state_array[1]]right_ee_state = [dual_gripper_state_array[0]]left_hand_action = [dual_gripper_action_array[1]]right_hand_action = [dual_gripper_action_array[0]]current_body_state = []current_body_action = []elif args.ee == "gripper" and args.xr_mode == 'controller':with dual_gripper_data_lock:left_ee_state = [dual_gripper_state_array[1]]right_ee_state = [dual_gripper_state_array[0]]left_hand_action = [dual_gripper_action_array[1]]right_hand_action = [dual_gripper_action_array[0]]current_body_state = arm_ctrl.get_current_motor_q().tolist()current_body_action = [-tele_data.tele_state.left_thumbstick_value[1]  * 0.3,-tele_data.tele_state.left_thumbstick_value[0]  * 0.3,-tele_data.tele_state.right_thumbstick_value[0] * 0.3]elif args.ee == "inspire1" and args.xr_mode == 'hand':with dual_hand_data_lock:left_ee_state = dual_hand_state_array[:6]right_ee_state = dual_hand_state_array[-6:]left_hand_action = dual_hand_action_array[:6]right_hand_action = dual_hand_action_array[-6:]current_body_state = []current_body_action = []else:left_ee_state = []right_ee_state = []left_hand_action = []right_hand_action = []current_body_state = []current_body_action = []# 图像采集current_tv_image = tv_img_array.copy()if WRIST:current_wrist_image = wrist_img_array.copy()# 关节状态/动作left_arm_state  = current_lr_arm_q[:7]right_arm_state = current_lr_arm_q[-7:]left_arm_action = sol_q[:7]right_arm_action = sol_q[-7:]if is_recording:colors = {}depths = {}# 多相机/双目数据分流if BINOCULAR:colors[f"color_{0}"] = current_tv_image[:, :tv_img_shape[1]//2]colors[f"color_{1}"] = current_tv_image[:, tv_img_shape[1]//2:]if WRIST:colors[f"color_{2}"] = current_wrist_image[:, :wrist_img_shape[1]//2]colors[f"color_{3}"] = current_wrist_image[:, wrist_img_shape[1]//2:]else:colors[f"color_{0}"] = current_tv_imageif WRIST:colors[f"color_{1}"] = current_wrist_image[:, :wrist_img_shape[1]//2]colors[f"color_{2}"] = current_wrist_image[:, wrist_img_shape[1]//2:]# 组织各类状态states = {"left_arm": {                                                                    "qpos":   left_arm_state.tolist(),    # 关节角度"qvel":   [],                          "torque": [],                        }, "right_arm": {                                                                    "qpos":   right_arm_state.tolist(),       "qvel":   [],                          "torque": [],                         },                        "left_ee": {                                                                    "qpos":   left_ee_state,           "qvel":   [],                           "torque": [],                          }, "right_ee": {                                                                    "qpos":   right_ee_state,       "qvel":   [],                           "torque": [],  }, "body": {"qpos": current_body_state,}, }actions = {"left_arm": {                                   "qpos":   left_arm_action.tolist(),       "qvel":   [],       "torque": [],      }, "right_arm": {                                   "qpos":   right_arm_action.tolist(),       "qvel":   [],       "torque": [],       },                         "left_ee": {                                   "qpos":   left_hand_action,       "qvel":   [],       "torque": [],       }, "right_ee": {                                   "qpos":   right_hand_action,       "qvel":   [],       "torque": [], }, "body": {"qpos": current_body_action,}, }if args.sim:  # 仿真状态同时录制sim_state = sim_state_subscriber.read_data()            recorder.add_item(colors=colors, depths=depths, states=states, actions=actions, sim_state=sim_state)else:recorder.add_item(colors=colors, depths=depths, states=states, actions=actions)# 控制主循环帧率(frequency控制,每周期做sleep补偿)current_time = time.time()time_elapsed = current_time - start_timesleep_time = max(0, (1 / args.frequency) - time_elapsed)time.sleep(sleep_time)logger_mp.debug(f"main process sleep: {sleep_time}")
  • 核心循环依赖于全局状态管理,控制整体流程启动、结束、录制等逻辑

  • 通过 XR 设备采集动作/手势数据,结合当前机器人状态,实时求逆解控制

  • 支持仿真和实机的兼容、数据分流、录制、UI 预览等功能

  • 按设置频率同步主循环(保持控制与采样一致性)

1.9 异常处理与资源清理

    except KeyboardInterrupt:logger_mp.info("KeyboardInterrupt, exiting program...")  # 捕获Ctrl+C优雅退出finally:arm_ctrl.ctrl_dual_arm_go_home()     # 程序结束时归位if args.sim:sim_state_subscriber.stop_subscribe()tv_img_shm.close()tv_img_shm.unlink()if WRIST:wrist_img_shm.close()wrist_img_shm.unlink()if args.record:recorder.close()listen_keyboard_thread.join()logger_mp.info("Finally, exiting program...")exit(0)
  • 保证即使中断也能自动归位/释放机器人

  • 关闭所有共享内存、线程、数据记录器,防止资源泄露

2 相机适配与脚本测试

目前官方给的遥操作是另外一套相机:

宇树各个模块参考链接:https://github.com/unitreerobotics/xr_teleoperate?tab=readme-ov-file

宇树相机参考链接:[For reference only] http://e.tb.cn/h.TaZxgkpfWkNCakg?tk=KKz03Kyu04u

为了与官方测试最为相机,我采用的是 “120无畸变”

先记一下这个相机的适配过程

2.1 相机适配

1. 使用 v4l2-ctl 命令预览设备信息

首先可以确认相机设备确实存在并可以访问:

v4l2-ctl --list-devices

或直接查看格式和分辨率支持:

v4l2-ctl --device=/dev/video6 --list-formats-ext
v4l2-ctl --device=/dev/video7 --list-formats-ext

2. 使用 ffplay 快速查看

ffplay /dev/video6

如果没有 ffplay,先装一下:sudo apt install ffmpeg

3. 使用 opencv 脚本查看

写一个简单的脚本:

'''
#双目
import cv2cap = cv2.VideoCapture(0)
while True:ret, frame = cap.read()if not ret:break# 双目拆分(假设1280x480,左右各640x480)left = frame[:, :frame.shape[1]//2]right = frame[:, frame.shape[1]//2:]cv2.imshow('left', left)cv2.imshow('right', right)if cv2.waitKey(1) == ord('q'):break
cap.release()
cv2.destroyAllWindows()
'''#单目
import cv2cap = cv2.VideoCapture(0)
while True:ret, frame = cap.read()if not ret:print("No frame!")breakcv2.imshow("cam6", frame)if cv2.waitKey(1) == ord('q'):break
cap.release()
cv2.destroyAllWindows()

可以显示:

2.2 脚本测试

2.1 图像服务

开启 image_server.py,只需修改 config:

    config = {'fps': 30,'head_camera_type': 'opencv','head_camera_image_shape': [480, 1280],  # Head camera resolution'head_camera_id_numbers': [0],}

然后,新开一个 terminal 开启 image_client.py,只需修改 ip,具体根据 ifconfig 查看:

client = ImageClient(image_show = True, server_address='192.168.8.30', Unit_Test=False) # deployment test

2.2 主控脚本测试

切记:关掉 client!!!开启server!!!

如果端口占用就:

#查看进程
sudo lsof -i :8012
#kill 占用端口的进程
sudo kill -9 12345

或者:

sudo kill -9 $(sudo lsof -t -i:8012)

如果报错 Segment Fault,那就是 dds 通讯没起来,就添加 cyclonedds 到 ~/.bashrc,然后 source ~/.bashrc 立即生效

export CYCLONEDDS_HOME="/home/unitree/Documents/unitree_sdk2_python/cyclonedds/install"

检查一下:

echo $CYCLONEDDS_HOME

主控脚本修改 config:

        img_config = {'fps': 30,'head_camera_type': 'opencv','head_camera_image_shape': [480, 1280],  # Head camera resolution'head_camera_id_numbers': [0],}

最后,确保 G1 已经进入 调试模式(L2+R2),并启动主控脚本:

打开浏览器应用(ipad Safari 也可以),输入并访问网址:https://192.168.8.30:8012?ws=wss://192.168.8.30:8012,可以快乐的遥操作啦

http://www.xdnf.cn/news/16562.html

相关文章:

  • Go 客户端玩转 ES|QL API 直连与 Mapping Helpers 实战详解
  • 11、read_object_model_3d 读取点云
  • 预装Windows 11系统的新电脑怎么跳过联网验机
  • 预过滤环境光贴图制作教程:第四阶段 - Lambert 无权重预过滤(Stage 3)
  • 三、Linux用户与权限管理详解
  • Redis内存使用耗尽情况分析
  • 编辑距离:理论基础、算法演进与跨领域应用
  • Windows使用Powershell自动安装SqlServer2025服务器与SSMS管理工具
  • css3之三维变换详说
  • Qt 多线程界面更新策略
  • 如何在Windows操作系统上通过conda 安装 MDAnalysis
  • 激光雷达/相机一体机 时间同步和空间标定(1)
  • 自然语言处理NLP(3)
  • leetcode 74. 搜索二维矩阵
  • 柔性生产前端动态适配:小批量换型场景下的参数配置智能切换技术
  • 汇总10个高质量免费AI生成论文网站,支持GPT4.0和DeepSeek-R1
  • cpolar 内网穿透 ubuntu 使用石
  • 2025年06月 C/C++(二级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • go install报错: should be v0 or v1, not v2问题解决
  • 【自制组件库】从零到一实现属于自己的 Vue3 组件库!!!
  • P2910 [USACO08OPEN] Clear And Present Danger S
  • 四、Linux核心工具:Vim, 文件链接与SSH
  • 永磁同步电机无速度算法--静态补偿电压模型Harnefors观测器
  • 人工智能技术革命:AI工具与大模型如何重塑开发者工作模式与行业格局
  • Linux 完整删除 Systemd 服务的步骤
  • redis得到shell的几种方法
  • 如何使用Spring AI框架开发mcp接口并发布成微服务
  • 31.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--财务服务--收支分类
  • 解决IDEA拉取GitLab项目报错:必须为访问令牌授予作用域[api, read user]
  • 日语学习-日语知识点小记-构建基础-JLPT-N3阶段(11):文法+单词