Browse Source

Merge pull request #182 from unitreerobotics/teleimager

[v1.4] Teleimager, new televuer and more
main
silencht 4 months ago
committed by GitHub
parent
commit
afcc5fe728
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      .gitmodules
  2. 14
      CHANGELOG.md
  3. 20
      CHANGELOG_zh-CN.md
  4. 250
      README.md
  5. 210
      README_zh-CN.md
  6. 197
      teleop/image_server/image_client.py
  7. 321
      teleop/image_server/image_server.py
  8. 24
      teleop/robot_control/robot_arm.py
  9. 92
      teleop/robot_control/robot_arm_ik.py
  10. 2
      teleop/robot_control/robot_hand_brainco.py
  11. 189
      teleop/robot_control/robot_hand_inspire.py
  12. 53
      teleop/robot_control/robot_hand_unitree.py
  13. 1
      teleop/teleimager
  14. 376
      teleop/teleop_hand_and_arm.py
  15. 2
      teleop/televuer
  16. 10
      teleop/utils/episode_writer.py
  17. 45
      teleop/utils/ipc.py
  18. 54
      teleop/utils/motion_switcher.py

3
.gitmodules

@ -4,3 +4,6 @@
[submodule "teleop/robot_control/dex-retargeting"] [submodule "teleop/robot_control/dex-retargeting"]
path = teleop/robot_control/dex-retargeting path = teleop/robot_control/dex-retargeting
url = https://github.com/silencht/dex-retargeting url = https://github.com/silencht/dex-retargeting
[submodule "teleop/teleimager"]
path = teleop/teleimager
url = https://github.com/silencht/teleimager.git

14
CHANGELOG.md

@ -1,5 +1,19 @@
# 🔖 Release Note # 🔖 Release Note
## 🏷️ v1.4
- The **image server** has been changed to [teleimager](https://github.com/silencht/teleimager). Please refer to the repository README for details.
- [televuer](https://github.com/silencht/televuer) has been upgraded. Please see the repository README for details.
> The new versions of [teleimager](https://github.com/silencht/teleimager/commit/ab5018691943433c24af4c9a7f3ea0c9a6fbaf3c) + [televuer](https://github.com/silencht/televuer/releases/tag/v3.0) support transmitting head camera images via **WebRTC**.
- Enriched the task information parameters in **recording mode**, fixing and improving EpisodeWriter.
- Improved the system’s **state machine** information and IPC mode.
- Added **pass-through mode**, allowing direct viewing of the external environment through a VR device camera (without using the robot’s head camera).
- Added **CPU affinity mode**. If you are not familiar with this mode, you can ignore it.
- Added **motion-switcher** functionality, allowing automatic debug mode entry and exit without using a remote controller.
## 🏷️ v1.3 ## 🏷️ v1.3
- add [![Unitree LOGO](https://camo.githubusercontent.com/ff307b29fe96a9b115434a450bb921c2a17d4aa108460008a88c58a67d68df4e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4769744875622d57696b692d3138313731373f6c6f676f3d676974687562)](https://github.com/unitreerobotics/xr_teleoperate/wiki) [![Unitree LOGO](https://camo.githubusercontent.com/6f5253a8776090a1f89fa7815e7543488a9ec200d153827b4bc7c3cb5e1c1555/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2d446973636f72642d3538363546323f7374796c653d666c6174266c6f676f3d446973636f7264266c6f676f436f6c6f723d7768697465)](https://discord.gg/ZwcVwxv5rq) - add [![Unitree LOGO](https://camo.githubusercontent.com/ff307b29fe96a9b115434a450bb921c2a17d4aa108460008a88c58a67d68df4e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4769744875622d57696b692d3138313731373f6c6f676f3d676974687562)](https://github.com/unitreerobotics/xr_teleoperate/wiki) [![Unitree LOGO](https://camo.githubusercontent.com/6f5253a8776090a1f89fa7815e7543488a9ec200d153827b4bc7c3cb5e1c1555/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2d446973636f72642d3538363546323f7374796c653d666c6174266c6f676f3d446973636f7264266c6f676f436f6c6f723d7768697465)](https://discord.gg/ZwcVwxv5rq)

20
CHANGELOG_zh-CN.md

@ -1,5 +1,25 @@
# 🔖 版本说明 # 🔖 版本说明
## 🏷️ v1.4
- **图像服务器**变更为 [teleimager](https://github.com/silencht/teleimager),具体请查看仓库README。
- 升级 [televuer](https://github.com/silencht/televuer),具体请查看仓库README。
> 新版本的 [teleimager](https://github.com/silencht/teleimager/commit/ab5018691943433c24af4c9a7f3ea0c9a6fbaf3c) + [televuer](https://github.com/silencht/televuer/releases/tag/v3.0) 支持通过 **webrtc** 传输头部相机图像
>
> 支持 pass-through, ego, immersive 三种模式:pass-through 为通透模式,直接通过 VR 相机查看现实世界来观察机器人;ego 是在通透模式的基础上,添加一个机器人视角的小窗;immersive 是完全沉浸机器人第一人称视角模式。
- 丰富**录制模式**下的任务信息传递参数,修复和完善 EpisodeWriter。
- 完善系统的**状态机信息**、IPC模式。
- 新增 **affinity CPU 亲和模式**,如果你不了解该模式,那么请无视它。
- 新增 **motion-switcher 功能**,无需遥控器即可自动进退 debug 模式。
- 支持 **inspire_FTP** 灵巧手
## 🏷️ v1.3 ## 🏷️ v1.3
- 添加 [![Unitree LOGO](https://camo.githubusercontent.com/ff307b29fe96a9b115434a450bb921c2a17d4aa108460008a88c58a67d68df4e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4769744875622d57696b692d3138313731373f6c6f676f3d676974687562)](https://github.com/unitreerobotics/xr_teleoperate/wiki) [![Unitree LOGO](https://camo.githubusercontent.com/6f5253a8776090a1f89fa7815e7543488a9ec200d153827b4bc7c3cb5e1c1555/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2d446973636f72642d3538363546323f7374796c653d666c6174266c6f676f3d446973636f7264266c6f676f436f6c6f723d7768697465)](https://discord.gg/ZwcVwxv5rq) - 添加 [![Unitree LOGO](https://camo.githubusercontent.com/ff307b29fe96a9b115434a450bb921c2a17d4aa108460008a88c58a67d68df4e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4769744875622d57696b692d3138313731373f6c6f676f3d676974687562)](https://github.com/unitreerobotics/xr_teleoperate/wiki) [![Unitree LOGO](https://camo.githubusercontent.com/6f5253a8776090a1f89fa7815e7543488a9ec200d153827b4bc7c3cb5e1c1555/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2d446973636f72642d3538363546323f7374796c653d666c6174266c6f676f3d446973636f7264266c6f676f436f6c6f723d7768697465)](https://discord.gg/ZwcVwxv5rq)

250
README.md

@ -36,15 +36,20 @@
# 🔖[Release Note](CHANGELOG.md) # 🔖[Release Note](CHANGELOG.md)
## 🏷️ v1.3
## 🏷️ v1.4
- add [![Unitree LOGO](https://camo.githubusercontent.com/ff307b29fe96a9b115434a450bb921c2a17d4aa108460008a88c58a67d68df4e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4769744875622d57696b692d3138313731373f6c6f676f3d676974687562)](https://github.com/unitreerobotics/xr_teleoperate/wiki) [![Unitree LOGO](https://camo.githubusercontent.com/6f5253a8776090a1f89fa7815e7543488a9ec200d153827b4bc7c3cb5e1c1555/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2d446973636f72642d3538363546323f7374796c653d666c6174266c6f676f3d446973636f7264266c6f676f436f6c6f723d7768697465)](https://discord.gg/ZwcVwxv5rq)
- The **image server** has been changed to [teleimager](https://github.com/silencht/teleimager). Please refer to the repository README for details.
- Support **IPC mode**, defaulting to use SSHKeyboard for input control.
- Merged motion mode support for H1_2 robot.
- Merged motion mode support for the G1_23 robot arm.
- Upgraded [televuer](https://github.com/silencht/televuer). Please see the repository README for details.
- ···
> The new versions of [teleimager](https://github.com/silencht/teleimager/commit/ab5018691943433c24af4c9a7f3ea0c9a6fbaf3c) + [televuer](https://github.com/silencht/televuer/releases/tag/v3.0) support transmitting **head camera images via WebRTC**.
> Supports **pass-through**, **ego**, and **immersive** modes.
- Improved the system’s **state machine** information and IPC mode.
- Added support for **Inspire_FTP dexterous hand**.
- …
# 0. 📖 Introduction # 0. 📖 Introduction
@ -56,8 +61,8 @@ Additionally, the [Wiki of this repo](https://github.com/unitreerobotics/xr_tele
Here are the required devices and wiring diagram, Here are the required devices and wiring diagram,
<p align="center"> <p align="center">
<a href="https://oss-global-cdn.unitree.com/static/3f75e91e41694ed28c29bcad22954d1d_5990x4050.png">
<img src="https://oss-global-cdn.unitree.com/static/3f75e91e41694ed28c29bcad22954d1d_5990x4050.png" alt="System Diagram" style="width: 100%;">
<a href="https://oss-global-cdn.unitree.com/static/55fb9cd245854810889855010da296f7_3415x2465.png">
<img src="https://oss-global-cdn.unitree.com/static/55fb9cd245854810889855010da296f7_3415x2465.png" alt="System Diagram" style="width: 100%;">
</a> </a>
</p> </p>
@ -126,19 +131,55 @@ For more information, you can refer to [Official Documentation ](https://support
(tv) unitree@Host:~$ cd xr_teleoperate (tv) unitree@Host:~$ cd xr_teleoperate
# Shallow clone submodule # Shallow clone submodule
(tv) unitree@Host:~/xr_teleoperate$ git submodule update --init --depth 1 (tv) unitree@Host:~/xr_teleoperate$ git submodule update --init --depth 1
```
```bash
# Install teleimager submodule
(tv) unitree@Host:~/xr_teleoperate$ cd teleop/teleimager
(tv) unitree@Host:~/xr_teleoperate/teleop/teleimager$ pip install -e . --no-deps
```
```bash
# Install televuer submodule # Install televuer submodule
(tv) unitree@Host:~/xr_teleoperate$ cd teleop/televuer (tv) unitree@Host:~/xr_teleoperate$ cd teleop/televuer
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ pip install -e . (tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ pip install -e .
# Generate the certificate files required for televuer submodule
# Configure SSL certificates for the televuer module so that XR devices (e.g., Pico / Quest / Apple Vision Pro) can securely connect via HTTPS / WebRTC
# 1. Generate certificate files
# 1.1 For Pico / Quest XR devices
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem (tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem
# Install dex-retargeting submodule
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ cd ../robot_control/dex-retargeting/
(tv) unitree@Host:~/xr_teleoperate/teleop/robot_control/dex-retargeting$ pip install -e .
# Install additional dependencies required by this repo
(tv) unitree@Host:~/xr_teleoperate/teleop/robot_control/dex-retargeting$ cd ../../../
(tv) unitree@Host:~/xr_teleoperate$ pip install -r requirements.txt
# 1.2 For Apple Vision Pro
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl genrsa -out rootCA.key 2048
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 365 -out rootCA.pem -subj "/CN=xr-teleoperate"
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl genrsa -out key.pem 2048
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl req -new -key key.pem -out server.csr -subj "/CN=localhost"
# Create server_ext.cnf file with the following content (IP.2 should match your host IP, e.g., 192.168.123.2. Use ifconfig or similar to check)
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ vim server_ext.cnf
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 192.168.123.164
IP.2 = 192.168.123.2
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out cert.pem -days 365 -sha256 -extfile server_ext.cnf
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ ls
build cert.pem key.pem LICENSE pyproject.toml README.md rootCA.key rootCA.pem rootCA.srl server.csr server_ext.cnf src test
# Copy rootCA.pem to Apple Vision Pro via AirDrop and install it
# Enable firewall
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ sudo ufw allow 8012
# 2. Configure certificate paths, choose one method
# 2.1 User config directory (optional)
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ mkdir -p ~/.config/xr_teleoperate/
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ cp cert.pem key.pem ~/.config/xr_teleoperate/
# 2.2 Environment variables (optional)
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ echo 'export XR_TELEOP_CERT="$HOME/xr_teleoperate/teleop/televuer/cert.pem"' >> ~/.bashrc
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ echo 'export XR_TELEOP_KEY="$HOME/xr_teleoperate/teleop/televuer/key.pem"' >> ~/.bashrc
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ source ~/.bashrc
``` ```
## 1.2 🕹️ unitree_sdk2_python ## 1.2 🕹️ unitree_sdk2_python
```bash ```bash
@ -165,10 +206,41 @@ For more information, you can refer to [Official Documentation ](https://support
> >
> You can refer to [Harley Hahn's Guide to Unix and Linux](https://www.harley.com/unix-book/book/chapters/04.html#H) and [Conda User Guide](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html) to learn more. > You can refer to [Harley Hahn's Guide to Unix and Linux](https://www.harley.com/unix-book/book/chapters/04.html#H) and [Conda User Guide](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html) to learn more.
## 1.3 🚀 Launch Parameter Description
- **Basic control parameters**
| ⚙️ Parameter | 📜 Description | 🔘 Available Options | 📌 Default |
| :---------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :---------------: |
| `--frequency` | Set the FPS for recording and control | Any reasonable float value | 30.0 |
| `--input-mode` | Choose XR input mode (how to control the robot) | `hand` (**hand tracking**)`controller` (**controller tracking**) | `hand` |
| `--display-mode` | Choose XR display mode (how to view the robot perspective) | `immersive` (immersive)`ego` (pass-through + small first-person window)`pass-through` (pass-through only) | `immersive` |
| `--arm` | Select the robot arm type (see 0. 📖 Introduction) | `G1_29``G1_23``H1_2``H1` | `G1_29` |
| `--ee` | Select the end-effector type of the arm (see 0. 📖 Introduction) | `dex1``dex3``inspire_ftp``inspire_dfx``brainco` | None |
| `--img-server-ip` | Set the image server IP address for receiving image streams and configuring WebRTC signaling | `IPv4` address | `192.168.123.164` |
- **Mode switch parameters**
| ⚙️ Parameter | 📜 Description |
| :----------: | :----------------------------------------------------------: |
| `--motion` | **Enable motion control mode**When enabled, the teleoperation program can run alongside the robot’s motion control program.In **hand tracking** mode, the [R3 controller](https://www.unitree.com/cn/R3) can be used to control normal robot walking; in **controller tracking** mode, joysticks can also control the robot’s movement. |
| `--headless` | **Enable headless mode**For running the program on devices without a display, e.g., the Development Computing Unit (PC2). |
| `--sim` | **Enable [simulation mode](https://github.com/unitreerobotics/unitree_sim_isaaclab)** |
| `--ipc` | **Inter-process communication mode**Allows controlling the xr_teleoperate program’s state via IPC. Suitable for interaction with agent programs. |
| `--affinity` | **CPU affinity mode**Set CPU core affinity. If you are unsure what this is, do not set it. |
| `--record` | **Enable data recording mode**Press **r** to start teleoperation, then **s** to start recording; press **s** again to stop and save the episode. Press **s** repeatedly to repeat the process. |
| `--task-*` | Configure the save path, target, description, and steps of the recorded task. |
# 2. 💻 Simulation Deployment # 2. 💻 Simulation Deployment
## 2.1 📥 Environment Setup ## 2.1 📥 Environment Setup
> Since the image service has been upgraded to `teleimager`, the simulation deployment for v1.4 is temporarily unavailable. Please use v1.3 for testing for now.
First, install [unitree_sim_isaaclab](https://github.com/unitreerobotics/unitree_sim_isaaclab). Follow that repo’s README. First, install [unitree_sim_isaaclab](https://github.com/unitreerobotics/unitree_sim_isaaclab). Follow that repo’s README.
Then launch the simulation with a G1(29 DoF) and Dex3 hand configuration: Then launch the simulation with a G1(29 DoF) and Dex3 hand configuration:
@ -193,23 +265,6 @@ Here is the simulation GUI:
This program supports XR control of a physical robot or in simulation. Choose modes with command-line arguments: This program supports XR control of a physical robot or in simulation. Choose modes with command-line arguments:
- **Basic control parameters**
| ⚙️ Parameter | 📜 Description | 🔘 Options | 📌 Default |
| :---------: | :-------------------------------------------: | :----------------------------------------------------------: | :-------: |
| `--xr-mode` | Choose XR input mode | `hand` (**hand tracking**)<br/>`controller` (**controller tracking**) | `hand` |
| `--arm` | Choose robot arm type (see 0. 📖 Introduction) | `G1_29`<br/>`G1_23`<br/>`H1_2`<br/>`H1` | `G1_29` |
| `--ee` | Choose end-effector (see 0. 📖 Introduction) | `dex1`<br/>`dex3`<br/>`inspire1`<br />`brainco` | none |
- **Mode flags**
| ⚙️ Flag | 📜 Description |
| :----------: | :----------------------------------------------------------: |
| `--record` | Enable **data recording**<br />After pressing **r** to start, press **s** to start/stop saving an episode. Can repeat. |
| `--motion` | Enable **motion mode**<br />After enabling this mode, the teleoperation program can run alongside the robot's motion control.<br />In **hand tracking** mode, you can use the [R3 Controller](https://www.unitree.com/cn/R3) to control the robot's walking behavior; <br />in **controller tracking** mode, you can also use [controllers to control the robot’s movement](https://github.com/unitreerobotics/xr_teleoperate/blob/375cdc27605de377c698e2b89cad0e5885724ca6/teleop/teleop_hand_and_arm.py#L247-L257). |
| `--headless` | Run without GUI (for headless PC2 deployment) |
| `--sim` | Enable **simulation mode** |
Assuming hand tracking with G1(29 DoF) + Dex3 in simulation with recording: Assuming hand tracking with G1(29 DoF) + Dex3 in simulation with recording:
```bash ```bash
@ -229,7 +284,20 @@ Next steps:
2. Connect to the corresponding Wi‑Fi 2. Connect to the corresponding Wi‑Fi
3. Open a browser (e.g. Safari or PICO Browser) and go to: `https://192.168.123.2:8012?ws=wss://192.168.123.2:8012`
3. Only proceed if your head camera has WebRTC enabled (`cam_config_server.yaml → head_camera → enable_webrtc: true`); otherwise jump to Step 4. Open a browser (e.g. Safari or PICO Browser) and go to:
**https://192.168.123.164:60001**
> **Note 1:** This IP is the address of **PC2**—the machine running teleimager service.
> **Note 2:** You may see a warning page like step 4. Click **Advanced**, then **Proceed to IP (unsafe)**. Once the page loads, press the **start** button in the top-left corner; if you see the head-camera preview, the check is successful.
> **Note 3:** This step serves two purposes:
>
> 1. Verify that the teleimager service is running correctly.
> 2. Manually trust the WebRTC self-signed certificate.
>
> Once this has been done on the same device with the same certificate, you can skip it on subsequent launches.
4. Open a browser (e.g. Safari or PICO Browser) and go to: `https://192.168.123.2:8012/?ws=wss://192.168.123.2:8012`
> **Note 1**: This IP must match your **Host** IP (check with `ifconfig`). > **Note 1**: This IP must match your **Host** IP (check with `ifconfig`).
> >
@ -241,11 +309,11 @@ Next steps:
</a> </a>
</p> </p>
4. In the Vuer web, click **Virtual Reality**. Allow all prompts to start the VR session.
5. In the Vuer web, click **Virtual Reality**. Allow all prompts to start the VR session.
<p align="center"> <a href="https://oss-global-cdn.unitree.com/static/fdeee4e5197f416290d8fa9ecc0b28e6_2480x1286.png"> <img src="https://oss-global-cdn.unitree.com/static/fdeee4e5197f416290d8fa9ecc0b28e6_2480x1286.png" alt="Vuer UI" style="width: 75%;"> </a> </p> <p align="center"> <a href="https://oss-global-cdn.unitree.com/static/fdeee4e5197f416290d8fa9ecc0b28e6_2480x1286.png"> <img src="https://oss-global-cdn.unitree.com/static/fdeee4e5197f416290d8fa9ecc0b28e6_2480x1286.png" alt="Vuer UI" style="width: 75%;"> </a> </p>
5. You’ll see the robot’s first-person view in the headset. The terminal prints connection info:
6. You’ll see the robot’s first-person view in the headset. The terminal prints connection info:
```bash ```bash
websocket is connected. id:dbb8537d-a58c-4c57-b49d-cbb91bd25b90 websocket is connected. id:dbb8537d-a58c-4c57-b49d-cbb91bd25b90
@ -253,19 +321,21 @@ Next steps:
Uplink task running. id:dbb8537d-a58c-4c57-b49d-cbb91bd25b90 Uplink task running. id:dbb8537d-a58c-4c57-b49d-cbb91bd25b90
``` ```
6. Align your arm to the **robot’s initial pose** to avoid sudden movements at start:
7. Align your arm to the **robot’s initial pose** to avoid sudden movements at start:
<p align="center"> <a href="https://oss-global-cdn.unitree.com/static/2522a83214744e7c8c425cc2679a84ec_670x867.png"> <img src="https://oss-global-cdn.unitree.com/static/2522a83214744e7c8c425cc2679a84ec_670x867.png" alt="Initial Pose" style="width: 25%;"> </a> </p> <p align="center"> <a href="https://oss-global-cdn.unitree.com/static/2522a83214744e7c8c425cc2679a84ec_670x867.png"> <img src="https://oss-global-cdn.unitree.com/static/2522a83214744e7c8c425cc2679a84ec_670x867.png" alt="Initial Pose" style="width: 25%;"> </a> </p>
7. Press **r** in the terminal to begin teleoperation. You can now control the robot arm and dexterous hand.
8. Press **r** in the terminal to begin teleoperation. You can now control the robot arm and dexterous hand.
8. During teleoperation, press **s** to start recording; press **s** again to stop and save. Repeatable process.
9. During teleoperation, press **s** to start recording; press **s** again to stop and save. Repeatable process.
<p align="center"> <a href="https://oss-global-cdn.unitree.com/static/f5b9b03df89e45ed8601b9a91adab37a_2397x1107.png"> <img src="https://oss-global-cdn.unitree.com/static/f5b9b03df89e45ed8601b9a91adab37a_2397x1107.png" alt="Recording Process" style="width: 75%;"> </a> </p> <p align="center"> <a href="https://oss-global-cdn.unitree.com/static/f5b9b03df89e45ed8601b9a91adab37a_2397x1107.png"> <img src="https://oss-global-cdn.unitree.com/static/f5b9b03df89e45ed8601b9a91adab37a_2397x1107.png" alt="Recording Process" style="width: 75%;"> </a> </p>
> **Note 1**: Recorded data is stored in `xr_teleoperate/teleop/utils/data` by default, with usage instructions at this repo: [unitree_IL_lerobot](https://github.com/unitreerobotics/unitree_IL_lerobot/tree/main?tab=readme-ov-file#data-collection-and-conversion). > **Note 1**: Recorded data is stored in `xr_teleoperate/teleop/utils/data` by default, with usage instructions at this repo: [unitree_IL_lerobot](https://github.com/unitreerobotics/unitree_IL_lerobot/tree/main?tab=readme-ov-file#data-collection-and-conversion).
> >
> **Note 2**: Please pay attention to your disk space size during data recording. > **Note 2**: Please pay attention to your disk space size during data recording.
>
> **Note 3**: In v1.4 and above, the “record image” window has been removed.
## 2.3 🔚 Exit ## 2.3 🔚 Exit
@ -281,34 +351,50 @@ Physical deployment steps are similar to simulation, with these key differences:
In the simulation environment, the image service is automatically enabled. For physical deployment, you need to manually start the image service based on your specific camera hardware. The steps are as follows: In the simulation environment, the image service is automatically enabled. For physical deployment, you need to manually start the image service based on your specific camera hardware. The steps are as follows:
Copy `image_server.py` in the `xr_teleoperate/teleop/image_server` directory to the **Development Computing Unit PC2** of Unitree Robot (G1/H1/H1_2/etc.),
1. Install the image service program on the **Development Computing Unit PC2** of the Unitree robot (G1/H1/H1_2, etc.)
```bash
# p.s. You can transfer image_server.py to PC2 via the scp command and then use ssh to remotely login to PC2 to execute it.
# Assuming the IP address of the development computing unit PC2 is 192.168.123.164, the transmission process is as follows:
# log in to PC2 via SSH and create the folder for the image server
(tv) unitree@Host:~$ ssh unitree@192.168.123.164 "mkdir -p ~/image_server"
# Copy the local image_server.py to the ~/image_server directory on PC2
(tv) unitree@Host:~$ scp ~/xr_teleoperate/teleop/image_server/image_server.py unitree@192.168.123.164:~/image_server/
```
```bash
# SSH into PC2 and download the image service repository
and execute the following command **in the PC2**:
(base) unitree@PC2:~$ cd ~
(base) unitree@PC2:~$ git clone https://github.com/silencht/teleimager
```bash
# p.s. Currently, this image transmission program supports two methods for reading images: OpenCV and Realsense SDK. Please refer to the comments in the `ImageServer` class within `image_server.py` to configure your image transmission service according to your camera hardware.
# Now located in Unitree Robot PC2 terminal
unitree@PC2:~/image_server$ python image_server.py
# You can see the terminal output as follows:
# {'fps': 30, 'head_camera_type': 'opencv', 'head_camera_image_shape': [480, 1280], 'head_camera_id_numbers': [0]}
# [Image Server] Head camera 0 resolution: 480.0 x 1280.0
# [Image Server] Image server has started, waiting for client connections...
```
# Configure the environment according to the instructions in the teleimager repository README: https://github.com/silencht/teleimager/blob/main/README.md
```
2. On the **local host**, execute the following commands:
```bash
# Copy the `key.pem` and `cert.pem` files configured in Section 1.1 from the **local host** `xr_teleoperate/teleop/televuer` directory to the corresponding path on PC2
# These two files are required by teleimager to start the WebRTC service
(tv) unitree@Host:~$ scp ~/xr_teleoperate/teleop/televuer/key.pem ~/xr_teleoperate/teleop/televuer/cert.pem unitree@192.168.123.164:~/teleimager
# On PC2, configure the certificate path according to the teleimager repository README, for example:
(teleimager) unitree@PC2:~$ cd teleimager
(teleimager) unitree@PC2:~$ mkdir -p ~/.config/xr_teleoperate/
(teleimager) unitree@PC2:~/teleimager$ cp cert.pem key.pem ~/.config/xr_teleoperate/
```
3. On the **development computing unit PC2**, configure `cam_config_server.yaml` according to the teleimager documentation and start the image service.
```bash
(teleimager) unitree@PC2:~/image_server$ python -m teleimager.image_server
# The following command works the same way
(teleimager) unitree@PC2:~/image_server$ teleimager-server
```
4. On the **local host**, execute the following command to subscribe to the images
```bash
(tv) unitree@Host:~$ cd ~/xr_teleoperate/teleop/teleimager/src
(tv) unitree@Host:~/xr_teleoperate/teleop/teleimager/src$ python -m teleimager.image_client --host 192.168.123.164
# If the WebRTC image stream is set up, you can also open the URL [https://192.168.123.164:60001](https://192.168.123.164:60001) in a browser and click the Start button to test.
```
After image service is started, you can use `image_client.py` **in the Host** terminal to test whether the communication is successful:
```bash
(tv) unitree@Host:~/xr_teleoperate/teleop/image_server$ python image_client.py
```
## 3.2 ✋ Inspire Hand Service (optional) ## 3.2 ✋ Inspire Hand Service (optional)
@ -316,7 +402,7 @@ After image service is started, you can use `image_client.py` **in the Host** te
> >
> **Note 2**: For G1 robot with [Inspire DFX hand](https://support.unitree.com/home/zh/G1_developer/inspire_dfx_dexterous_hand), related issue [#46](https://github.com/unitreerobotics/xr_teleoperate/issues/46). > **Note 2**: For G1 robot with [Inspire DFX hand](https://support.unitree.com/home/zh/G1_developer/inspire_dfx_dexterous_hand), related issue [#46](https://github.com/unitreerobotics/xr_teleoperate/issues/46).
> >
> **Note 3**: For [Inspire FTP hand]((https://support.unitree.com/home/zh/G1_developer/inspire_ftp_dexterity_hand)), related issue [#48](https://github.com/unitreerobotics/xr_teleoperate/issues/48).
> **Note 3**: For [Inspire FTP hand]((https://support.unitree.com/home/zh/G1_developer/inspire_ftp_dexterity_hand)), related issue [#48](https://github.com/unitreerobotics/xr_teleoperate/issues/48). FTP dexterous hand is now supported. Please refer to the `--ee` parameter for configuration.
First, use [this URL: DFX_inspire_service](https://github.com/unitreerobotics/DFX_inspire_service) to clone the dexterous hand control interface program. And Copy it to **PC2** of Unitree robots. First, use [this URL: DFX_inspire_service](https://github.com/unitreerobotics/DFX_inspire_service) to clone the dexterous hand control interface program. And Copy it to **PC2** of Unitree robots.
@ -340,6 +426,8 @@ unitree@PC2:~/DFX_inspire_service/build$ ./hand_example
If two hands open and close continuously, it indicates success. Once successful, close the `./hand_example` program in Terminal 2. If two hands open and close continuously, it indicates success. Once successful, close the `./hand_example` program in Terminal 2.
## 3.3 ✋ BrainCo Hand Service (Optional) ## 3.3 ✋ BrainCo Hand Service (Optional)
Please refer to the [official documentation](https://support.unitree.com/home/en/G1_developer/brainco_hand) for setup instructions. Please refer to the [official documentation](https://support.unitree.com/home/en/G1_developer/brainco_hand) for setup instructions.
@ -361,8 +449,7 @@ sudo ./brainco_hand --id 127 --serial /dev/ttyUSB2
> >
> 1. Everyone must keep a safe distance from the robot to prevent any potential danger! > 1. Everyone must keep a safe distance from the robot to prevent any potential danger!
> 2. Please make sure to read the [Official Documentation](https://support.unitree.com/home/zh/Teleoperation) at least once before running this program. > 2. Please make sure to read the [Official Documentation](https://support.unitree.com/home/zh/Teleoperation) at least once before running this program.
> 3. Without `--motion`, always make sure that the robot has entered [debug mode (L2+R2)](https://support.unitree.com/home/zh/H1_developer/Remote_control) to stop the motion control program, this will avoid potential command conflict problems.
> 4. To use motion mode (with `--motion`), ensure the robot is in control mode (via [R3 remote](https://www.unitree.com/R3)).
> 3. To use motion mode (with `--motion`), ensure the robot is in control mode (via [R3 remote](https://www.unitree.com/R3)).
> 5. In motion mode: > 5. In motion mode:
> - Right controller **A** = Exit teleop > - Right controller **A** = Exit teleop
> - Both joysticks pressed = soft emergency stop (switch to damping mode) > - Both joysticks pressed = soft emergency stop (switch to damping mode)
@ -391,37 +478,37 @@ Same as simulation but follow the safety warnings above.
``` ```
xr_teleoperate/ xr_teleoperate/
├── assets [Storage of robot URDF-related files]
├── hardware [3D‑printed hardware modules]
├── assets [Stores robot URDF-related files]
├── teleop ├── teleop
│ ├── image_server
│ │ ├── image_client.py [Used to receive image data from the robot image server]
│ │ ├── image_server.py [Capture images from cameras and send via network (Running on robot's Development Computing Unit PC2)]
│ ├── teleimager [New image service library, supporting multiple features]
│ │ │ │
│ ├── televuer │ ├── televuer
│ │ ├── src/televuer │ │ ├── src/televuer
│ │ ├── television.py [captures XR devices's head, wrist, hand/controller data]
│ │ ├── television.py [Captures head, wrist, and hand/controller data from XR devices using Vuer]
│ │ ├── tv_wrapper.py [Post-processing of captured data] │ │ ├── tv_wrapper.py [Post-processing of captured data]
│ │ ├── test │ │ ├── test
│ │ ├── _test_television.py [test for television.py]
│ │ ├── _test_tv_wrapper.py [test for tv_wrapper.py]
│ │ ├── _test_television.py [Test program for television.py]
│ │ ├── _test_tv_wrapper.py [Test program for tv_wrapper.py]
│ │ │ │
│ ├── robot_control │ ├── robot_control
│ │ ├── src/dex-retargeting [Dexterous hand retargeting algorithm library] │ │ ├── src/dex-retargeting [Dexterous hand retargeting algorithm library]
│ │ ├── robot_arm_ik.py [Inverse kinematics of the arm]
│ │ ├── robot_arm.py [Control dual arm joints and lock the others]
│ │ ├── hand_retargeting.py [Dexterous hand retargeting algorithm library Wrapper]
│ │ ├── robot_hand_inspire.py [Control inspire hand joints]
│ │ ├── robot_hand_unitree.py [Control unitree hand joints]
│ │ ├── robot_arm_ik.py [Inverse kinematics for the arm]
│ │ ├── robot_arm.py [Controls dual-arm joints and locks other parts]
│ │ ├── hand_retargeting.py [Wrapper for the dexterous hand retargeting library]
│ │ ├── robot_hand_inspire.py [Controls Inspire dexterous hand]
│ │ ├── robot_hand_unitree.py [Controls Unitree dexterous hand]
│ │ │ │
│ ├── utils │ ├── utils
│ │ ├── episode_writer.py [Used to record data for imitation learning] │ │ ├── episode_writer.py [Used to record data for imitation learning]
│ │ ├── weighted_moving_filter.py [For filtering joint data]
│ │ ├── rerun_visualizer.py [For visualizing data during recording]
│ │ ├── weighted_moving_filter.py [Filter for joint data]
│ │ ├── rerun_visualizer.py [Visualizes recorded data]
│ │ ├── ipc.py [Handles inter-process communication with proxy programs]
│ │ ├── motion_switcher.py [Switches motion control states]
│ │ ├── sim_state_topic.py [For simulation deployment]
│ │ │ │
│ └── teleop_hand_and_arm.py [Startup execution code for teleoperation]
│ └── teleop_hand_and_arm.py [Startup script for teleoperation]
``` ```
# 5. 🛠️ Hardware # 5. 🛠️ Hardware
@ -443,3 +530,4 @@ This code builds upon following open-source code-bases. Please visit the URLs to
7. https://github.com/zeromq/pyzmq 7. https://github.com/zeromq/pyzmq
8. https://github.com/Dingry/BunnyVisionPro 8. https://github.com/Dingry/BunnyVisionPro
9. https://github.com/unitreerobotics/unitree_sdk2_python 9. https://github.com/unitreerobotics/unitree_sdk2_python
10. https://github.com/ARCLab-MIT/beavr-bot

210
README_zh-CN.md

@ -36,13 +36,19 @@
# 🔖 [版本说明](CHANGELOG_zh-CN.md) # 🔖 [版本说明](CHANGELOG_zh-CN.md)
## 🏷️ v1.3
## 🏷️ v1.4
- 添加 [![Unitree LOGO](https://camo.githubusercontent.com/ff307b29fe96a9b115434a450bb921c2a17d4aa108460008a88c58a67d68df4e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4769744875622d57696b692d3138313731373f6c6f676f3d676974687562)](https://github.com/unitreerobotics/xr_teleoperate/wiki) [![Unitree LOGO](https://camo.githubusercontent.com/6f5253a8776090a1f89fa7815e7543488a9ec200d153827b4bc7c3cb5e1c1555/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2d446973636f72642d3538363546323f7374796c653d666c6174266c6f676f3d446973636f7264266c6f676f436f6c6f723d7768697465)](https://discord.gg/ZwcVwxv5rq)
- **图像服务器**变更为 [teleimager](https://github.com/silencht/teleimager),具体请查看仓库README。
- 支持 **IPC 模式**,默认使用 SSHKeyboard 进行输入控制。
- 合并 **H1_2** 机器人新增运动模式支持。
- 合并 **G1_23** 机械臂新增运动模式支持。
- 升级 [televuer](https://github.com/silencht/televuer),具体请查看仓库README。
> 新版本的 [teleimager](https://github.com/silencht/teleimager/commit/ab5018691943433c24af4c9a7f3ea0c9a6fbaf3c) + [televuer](https://github.com/silencht/televuer/releases/tag/v3.0) 支持通过 webrtc 传输头部相机图像
>
> 支持 pass-through, ego, immersive 三种模式
- 完善系统的**状态机**信息、IPC模式。
- 支持 **inspire_FTP** 灵巧手。
- ··· - ···
@ -57,11 +63,12 @@
以下是系统示意图: 以下是系统示意图:
<p align="center"> <p align="center">
<a href="https://oss-global-cdn.unitree.com/static/54cff8fe11ac4158b9b15a73f0842843_5990x4060.png">
<img src="https://oss-global-cdn.unitree.com/static/54cff8fe11ac4158b9b15a73f0842843_5990x4060.png" alt="Watch the Document" style="width: 100%;">
<a href="https://oss-global-cdn.unitree.com/static/1804a35aa09a44a9bf9821fafc4a2348_3415x2465.png">
<img src="https://oss-global-cdn.unitree.com/static/1804a35aa09a44a9bf9821fafc4a2348_3415x2465.png" alt="Watch the Document" style="width: 100%;">
</a> </a>
</p> </p>
以下是本仓库目前支持的设备类型: 以下是本仓库目前支持的设备类型:
<table> <table>
@ -126,14 +133,59 @@
(tv) unitree@Host:~$ cd xr_teleoperate (tv) unitree@Host:~$ cd xr_teleoperate
# 浅克隆子模块 # 浅克隆子模块
(tv) unitree@Host:~/xr_teleoperate$ git submodule update --init --depth 1 (tv) unitree@Host:~/xr_teleoperate$ git submodule update --init --depth 1
```
```bash
# 安装 teleimager 模块
(tv) unitree@Host:~/xr_teleoperate$ cd teleop/teleimager
(tv) unitree@Host:~/xr_teleoperate/teleop/teleimager$ pip install -e . --no-deps
```
```bash
# 安装 televuer 模块 # 安装 televuer 模块
(tv) unitree@Host:~/xr_teleoperate$ cd teleop/televuer (tv) unitree@Host:~/xr_teleoperate$ cd teleop/televuer
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ pip install -e . (tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ pip install -e .
# 生成 televuer 模块所需的证书文件
# 为 televuer 模块配置 SSL 证书,以便 XR 设备(如 Pico / Quest / Apple Vision Pro)通过 HTTPS / WebRTC 安全连接
# 1. 生成证书文件
# 1.1 如果您使用 pico / quest 等 xr 设备
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem (tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem
# 1.2 如果您使用 apple vision pro 设备
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl genrsa -out rootCA.key 2048
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 365 -out rootCA.pem -subj "/CN=xr-teleoperate"
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl genrsa -out key.pem 2048
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl req -new -key key.pem -out server.csr -subj "/CN=localhost"
## 创建 server_ext.cnf 文件,输入以下内容(IP.2 地址应与您的 主机 IP 地址匹配,假设此处地址为 192.168.123.2。可以使用 `ifconfig` 等类似命令查询)
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ vim server_ext.cnf
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 192.168.123.164
IP.2 = 192.168.123.2
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out cert.pem -days 365 -sha256 -extfile server_ext.cnf
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ ls
build cert.pem key.pem LICENSE pyproject.toml README.md rootCA.key rootCA.pem rootCA.srl server.csr server_ext.cnf src test
# 通过 AirDrop 将 rootCA.pem 复制到 Apple Vision Pro 并安装它
# 开启防火墙
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ sudo ufw allow 8012
# 2. 配置证书路径,以下方式任选其一
# 2.1 用户配置目录(可选)
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ mkdir -p ~/.config/xr_teleoperate/
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ cp cert.pem key.pem ~/.config/xr_teleoperate/
# 2.2 环境变量配置(可选)
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ echo 'export XR_TELEOP_CERT="$HOME/xr_teleoperate/teleop/televuer/cert.pem"' >> ~/.bashrc
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ echo 'export XR_TELEOP_KEY="$HOME/xr_teleoperate/teleop/televuer/key.pem"' >> ~/.bashrc
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ source ~/.bashrc
```
```bash
# 安装 dex-retargeting 模块 # 安装 dex-retargeting 模块
(tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ cd ../robot_control/dex-retargeting/ (tv) unitree@Host:~/xr_teleoperate/teleop/televuer$ cd ../robot_control/dex-retargeting/
(tv) unitree@Host:~/xr_teleoperate/teleop/robot_control/dex-retargeting$ pip install -e . (tv) unitree@Host:~/xr_teleoperate/teleop/robot_control/dex-retargeting$ pip install -e .
```
```bash
# 安装本仓库所需的其他依赖库 # 安装本仓库所需的其他依赖库
(tv) unitree@Host:~/xr_teleoperate/teleop/robot_control/dex-retargeting$ cd ../../../ (tv) unitree@Host:~/xr_teleoperate/teleop/robot_control/dex-retargeting$ cd ../../../
(tv) unitree@Host:~/xr_teleoperate$ pip install -r requirements.txt (tv) unitree@Host:~/xr_teleoperate$ pip install -r requirements.txt
@ -168,12 +220,42 @@
> >
> 您可以参考 [Harley Hahn's Guide to Unix and Linux](https://www.harley.com/unix-book/book/chapters/04.html#H) 和 [Conda User Guide](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html) 来深入了解这些知识。 > 您可以参考 [Harley Hahn's Guide to Unix and Linux](https://www.harley.com/unix-book/book/chapters/04.html#H) 和 [Conda User Guide](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html) 来深入了解这些知识。
## 1.3 🚀 启动参数说明
- 基础控制参数
| ⚙️ 参数 | 📜 说明 | 🔘 目前可选值 | 📌 默认值 |
| :---------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :---------------: |
| `--frequency` | 设置录制和控制的 FPS | 任意正常范围内的浮点数 | 30.0 |
| `--input-mode` | 选择 XR 输入模式(通过什么方式控制机器人) | `hand`(**手势跟踪**)<br />`controller`(**手柄跟踪**) | `hand` |
| `--display-mode` | 选择 XR 显示模式(通过什么方式查看机器人视角) | `immersive`(沉浸式)<br />`ego`(通透+第一人称小窗)<br />`pass-through`(通透) | `immersive` |
| `--arm` | 选择机器人设备类型(可参考 0. 📖 介绍) | `G1_29`<br />`G1_23`<br />`H1_2`<br />`H1` | `G1_29` |
| `--ee` | 选择手臂的末端执行器设备类型(可参考 0. 📖 介绍) | `dex1`<br />`dex3`<br />`inspire_ftp`<br />`inspire_dfx`<br />`brainco` | 无默认值 |
| `--img-server-ip` | 设置图像服务器的 IP 地址,用于接收图像服务流、配置 WebRTC 信令服务地址 | `IPv4` 地址 | `192.168.123.164` |
- 模式开关参数
| ⚙️ 参数 | 📜 说明 |
| :----------: | :----------------------------------------------------------: |
| `--motion` | 【启用**运动控制**模式】<br />开启本模式后,可在机器人运控程序运行下进行遥操作程序。<br />**手势跟踪**模式下,可使用 [R3遥控器](https://www.unitree.com/cn/R3) 控制机器人正常行走;**手柄跟踪**模式下,也可使用[手柄摇杆控制机器人行走](https://github.com/unitreerobotics/xr_teleoperate/blob/375cdc27605de377c698e2b89cad0e5885724ca6/teleop/teleop_hand_and_arm.py#L247-L257)。 |
| `--headless` | 【启用**无图形界面**模式】<br />适用于本程序部署在开发计算单元(PC2)等无显示器情况 |
| `--sim` | 【启用[**仿真模式**](https://github.com/unitreerobotics/unitree_sim_isaaclab)】 |
| `--ipc` | 【进程间通信模式】<br />可通过进程间通信来控制 xr_teleoperate 程序的状态切换,此模式适合与代理程序进行交互 |
| `--affinity` | 【CPU亲和模式】<br />设置 CPU 核心亲和性。如果你不知道这是什么,那么请不要设置它。 |
| `--record` | 【启用**数据录制**模式】<br />**r** 键进入遥操后,按 **s** 键可开启数据录制,再次按 **s** 键可结束录制并保存本次 episode 数据。<br />继续按下 **s** 键可重复前述过程。 |
| `--task-*` | 此类参数可配置录制的文件保存路径,任务目标、描述、步骤等信息 |
------
# 2. 💻 仿真部署 # 2. 💻 仿真部署
## 2.1 📥 环境配置 ## 2.1 📥 环境配置
> 因为图像服务升级为`teleimager`,v1.4 版本仿真部署暂未上线,请暂时使用 v1.3 进行测试
首先,请安装 [unitree_sim_isaaclab](https://github.com/unitreerobotics/unitree_sim_isaaclab)。具体安装步骤,可参考该仓库 README 文档。 首先,请安装 [unitree_sim_isaaclab](https://github.com/unitreerobotics/unitree_sim_isaaclab)。具体安装步骤,可参考该仓库 README 文档。
其次,启动 unitree_sim_isaaclab 仿真环境。假设使用 G1(29 DoF) 和 Dex3 灵巧手配置进行仿真,则启动命令示例如下: 其次,启动 unitree_sim_isaaclab 仿真环境。假设使用 G1(29 DoF) 和 Dex3 灵巧手配置进行仿真,则启动命令示例如下:
@ -204,28 +286,7 @@
本程序支持通过 XR 设备(比如手势或手柄)来控制实际机器人动作,也支持在虚拟仿真中运行。你可以根据需要,通过命令行参数来配置运行方式。 本程序支持通过 XR 设备(比如手势或手柄)来控制实际机器人动作,也支持在虚拟仿真中运行。你可以根据需要,通过命令行参数来配置运行方式。
以下是本程序的启动参数说明:
- 基础控制参数
| ⚙️ 参数 | 📜 说明 | 🔘 目前可选值 | 📌 默认值 |
| :---------: | :----------------------------------------------: | :------------------------------------------------------: | :------: |
| `--xr-mode` | 选择 XR 输入模式(通过什么方式控制机器人) | `hand`(**手势跟踪**)<br />`controller`(**手柄跟踪**) | `hand` |
| `--arm` | 选择机器人设备类型(可参考 0. 📖 介绍) | `G1_29`<br />`G1_23`<br />`H1_2`<br />`H1` | `G1_29` |
| `--ee` | 选择手臂的末端执行器设备类型(可参考 0. 📖 介绍) | `dex1`<br />`dex3`<br />`inspire1`<br />`brainco` | 无默认值 |
- 模式开关参数
| ⚙️ 参数 | 📜 说明 |
| :----------: | :----------------------------------------------------------: |
| `--record` | 【启用**数据录制**模式】<br />**r** 键进入遥操后,按 **s** 键可开启数据录制,再次按 **s** 键可结束录制并保存本次 episode 数据。<br />继续按下 **s** 键可重复前述过程。 |
| `--motion` | 【启用**运动控制**模式】<br />开启本模式后,可在机器人运控程序运行下进行遥操作程序。<br />**手势跟踪**模式下,可使用 [R3遥控器](https://www.unitree.com/cn/R3) 控制机器人正常行走;**手柄跟踪**模式下,也可使用[手柄摇杆控制机器人行走](https://github.com/unitreerobotics/xr_teleoperate/blob/375cdc27605de377c698e2b89cad0e5885724ca6/teleop/teleop_hand_and_arm.py#L247-L257)。 |
| `--headless` | 【启用**无图形界面**模式】<br />适用于本程序部署在开发计算单元(PC2)等无显示器情况 |
| `--sim` | 【启用[**仿真模式**](https://github.com/unitreerobotics/unitree_sim_isaaclab)】 |
------
根据上述参数说明以及仿真环境配置,我们假设选择**手势跟踪**来控制 G1(29 DoF) + Dex3 灵巧手设备,同时开启仿真模式和数据录制模式。
根据 1.3 节参数说明以及仿真环境配置,我们假设选择**手势跟踪**来控制 G1(29 DoF) + Dex3 灵巧手设备,同时开启仿真模式和数据录制模式。
则启动命令如下所示: 则启动命令如下所示:
@ -250,7 +311,21 @@
2. 连接对应的 WiFi 热点 2. 连接对应的 WiFi 热点
3. 打开浏览器应用(比如 Safari 或 PICO Browser),输入并访问网址:https://192.168.123.2:8012?ws=wss://192.168.123.2:8012
3. 如果您头部相机开启了WebRTC功能(`cam_config_server.yaml => head_camera => enable_webrtc: true`),那么执行此步骤,否则直接跳到第 4 步。打开浏览器应用(比如 Safari 或 PICO Browser),输入并访问网址:https://192.168.123.164:60001
> 注意1:此 IP 地址为开启teleimager图像服务的 PC2 设备 IP
> 注意2:此时可能弹出类似第4步相同的警告提示。请点击`Advanced`按钮后,继续点击 `Proceed to ip (unsafe)` 按钮,使用非安全方式继续登录WebRTC图像服务器。进入后,点击左上角`start`按钮,如果预览到头部相机图像,那么操作成功。
>
> <p align="center">
> <a href="https://oss-global-cdn.unitree.com/static/777f9c6f42d74eb2a6438d1509a73025_2475x1574.jpg">
> <img src="https://oss-global-cdn.unitree.com/static/777f9c6f42d74eb2a6438d1509a73025_2475x1574.jpg" alt="webrtc_unsafe" style="width: 50%;">
> </a>
> </p>
>
> 注意3:此步骤目的有两个:一是检测头部相机服务是否正常;二是手动信任 `webrtc` 自签名证书。相同设备与自签名证书条件下执行一次本步骤后,再次启动时可跳过该步。
4. 打开浏览器应用(比如 Safari 或 PICO Browser),输入并访问网址:https://192.168.123.2:8012/?ws=wss://192.168.123.2:8012
> 注意1:此 IP 地址应与您的 **主机** IP 地址匹配。该地址可以使用 `ifconfig` 等类似命令查询。 > 注意1:此 IP 地址应与您的 **主机** IP 地址匹配。该地址可以使用 `ifconfig` 等类似命令查询。
@ -262,7 +337,7 @@
</a> </a>
</p> </p>
4. 进入`Vuer`网页界面后,点击 **`Virtual Reality`** 按钮。在允许后续的所有对话框后,启动 VR 会话。界面如下图所示:
5. 进入`Vuer`网页界面后,点击 **`Virtual Reality`** 按钮。在允许后续的所有对话框后,启动 VR 会话。界面如下图所示:
<p align="center"> <p align="center">
<a href="https://oss-global-cdn.unitree.com/static/fdeee4e5197f416290d8fa9ecc0b28e6_2480x1286.png"> <a href="https://oss-global-cdn.unitree.com/static/fdeee4e5197f416290d8fa9ecc0b28e6_2480x1286.png">
@ -270,7 +345,7 @@
</a> </a>
</p> </p>
5. 此时,您将会在 XR 头显设备中看到机器人的第一人称视野。同时,终端打印出链接建立的信息:
6. 此时,您将会在 XR 头显设备中看到机器人的第一人称视野。同时,终端打印出链接建立的信息:
```bash ```bash
websocket is connected. id:dbb8537d-a58c-4c57-b49d-cbb91bd25b90 websocket is connected. id:dbb8537d-a58c-4c57-b49d-cbb91bd25b90
@ -278,7 +353,7 @@
Uplink task running. id:dbb8537d-a58c-4c57-b49d-cbb91bd25b90 Uplink task running. id:dbb8537d-a58c-4c57-b49d-cbb91bd25b90
``` ```
6. 然后,将手臂形状摆放到与**机器人初始姿态**相接近的姿势。这一步是为了避免在实物部署时,初始位姿差距过大导致机器人产生过大的摆动。
7. 然后,将手臂形状摆放到与**机器人初始姿态**相接近的姿势。这一步是为了避免在实物部署时,初始位姿差距过大导致机器人产生过大的摆动。
机器人初始姿态示意图如下: 机器人初始姿态示意图如下:
@ -288,9 +363,9 @@
</a> </a>
</p> </p>
7. 最后,在终端中按下 **r** 键后,正式开启遥操作程序。此时,您可以远程控制机器人的手臂(和灵巧手)
8. 最后,在终端中按下 **r** 键后,正式开启遥操作程序。此时,您可以远程控制机器人的手臂(和灵巧手)
8. 在遥操过程中,按 **s** 键可开启数据录制,再次按 **s** 键可结束录制并保存数据(该过程可重复)
9. 在遥操过程中,按 **s** 键可开启数据录制,再次按 **s** 键可结束录制并保存数据(该过程可重复)
数据录制过程示意图如下: 数据录制过程示意图如下:
@ -303,10 +378,12 @@
> 注意1:录制的数据默认存储在 `xr_teleoperate/teleop/utils/data` 中。数据使用说明见此仓库: [unitree_IL_lerobot](https://github.com/unitreerobotics/unitree_IL_lerobot/blob/main/README_zh.md#%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E4%B8%8E%E8%BD%AC%E6%8D%A2)。 > 注意1:录制的数据默认存储在 `xr_teleoperate/teleop/utils/data` 中。数据使用说明见此仓库: [unitree_IL_lerobot](https://github.com/unitreerobotics/unitree_IL_lerobot/blob/main/README_zh.md#%E6%95%B0%E6%8D%AE%E9%87%87%E9%9B%86%E4%B8%8E%E8%BD%AC%E6%8D%A2)。
> >
> 注意2:请在录制数据时注意您的硬盘空间大小。 > 注意2:请在录制数据时注意您的硬盘空间大小。
>
> 注意3: v1.4 及以上版本,record image窗口取消。
## 2.3 🔚 退出 ## 2.3 🔚 退出
要退出程序,可以在终端窗口(或 'record image' 窗口)中按下 **q** 键。
要退出程序,可以在终端窗口中按下 **q** 键。
@ -318,42 +395,52 @@
仿真环境中已经自动开启了图像服务。实物部署时,需要针对自身相机硬件类型,手动开启图像服务。步骤如下: 仿真环境中已经自动开启了图像服务。实物部署时,需要针对自身相机硬件类型,手动开启图像服务。步骤如下:
`xr_teleoperate/teleop/image_server` 目录中的 `image_server.py` 复制到宇树机器人(G1/H1/H1_2 等)的 **开发计算单元 PC2**
1. 在宇树机器人(G1/H1/H1_2 等)的 **开发计算单元 PC2** 中安装图像服务程序
```bash ```bash
# 提醒:可以通过scp命令将image_server.py传输到PC2,然后使用ssh远程登录PC2后执行它。
# 假设开发计算单元PC2的ip地址为192.168.123.164,那么传输过程示例如下:
# 先ssh登录PC2,创建图像服务器的文件夹
(tv) unitree@Host:~$ ssh unitree@192.168.123.164 "mkdir -p ~/image_server"
# 将本地的image_server.py拷贝至PC2的~/image_server目录下
(tv) unitree@Host:~$ scp ~/xr_teleoperate/teleop/image_server/image_server.py unitree@192.168.123.164:~/image_server/
# ssh登录PC2,下载图像服务程序仓库
(base) unitree@PC2:~$ cd ~
(base) unitree@PC2:~$ git clone https://github.com/silencht/teleimager
# 根据 teleimager 仓库的 https://github.com/silencht/teleimager/blob/main/README.md 文档说明来配置环境
``` ```
并在 **PC2** 上执行以下命令:
2. 在**本地主机**上执行以下命令:
```bash ```bash
# 提醒:目前该图像传输程序支持OpenCV和Realsense SDK两种读取图像的方式,请阅读image_server.py的ImageServer类的注释以便您根据自己的相机硬件来配置自己的图像传输服务。
# 现在位于宇树机器人 PC2 终端
unitree@PC2:~/image_server$ python image_server.py
# 您可以看到终端输出如下:
# {'fps': 30, 'head_camera_type': 'opencv', 'head_camera_image_shape': [480, 1280], 'head_camera_id_numbers': [0]}
# [Image Server] Head camera 0 resolution: 480.0 x 1280.0
# [Image Server] Image server has started, waiting for client connections...
# 将本地主机 xr_teleoperate/teleop/televuer 路径下在 1.1 节配置的 key.pem 和 cert.pem 文件拷贝到 PC2 对应路径
# 这两个文件是 teleimager 启动 WebRTC 服务时所必须的
(tv) unitree@Host:~$ scp ~/xr_teleoperate/teleop/televuer/key.pem ~/xr_teleoperate/teleop/televuer/cert.pem unitree@192.168.123.164:~/teleimager
# 根据 teleimager 仓库的 https://github.com/silencht/teleimager/blob/main/README.md 文档说明,在PC2配置证书路径,例如
(teleimager) unitree@PC2:~$ cd teleimager
(teleimager) unitree@PC2:~$ mkdir -p ~/.config/xr_teleoperate/
(teleimager) unitree@PC2:~/teleimager$ cp cert.pem key.pem ~/.config/xr_teleoperate/
``` ```
在图像服务启动后,您可以在 **主机** 终端上使用 `image_client.py` 测试通信是否成功:
3. 在**开发计算单元 PC2** 中按照 teleimager 文档配置 cam_config_server.yaml 并启动图像服务程序
```bash ```bash
(tv) unitree@Host:~/xr_teleoperate/teleop/image_server$ python image_client.py
(teleimager) unitree@PC2:~/image_server$ python -m teleimager.image_server
# 下面命令作用相同
(teleimager) unitree@PC2:~/image_server$ teleimager-server
``` ```
4. 在**本地主机**上执行以下命令订阅图像:
```bash
(tv) unitree@Host:~$ cd ~/xr_teleoperate/teleop/teleimager/src
(tv) unitree@Host:~/xr_teleoperate/teleop/teleimager/src$ python -m teleimager.image_client --host 192.168.123.164
# 如果设置了 WebRTC 图像流,那么可以在浏览器中通过 https://192.168.123.164:60001 打开网址,随后点击 Start 按钮进行测试
```
## 3.2 ✋ Inspire 手部服务(可选) ## 3.2 ✋ Inspire 手部服务(可选)
> 注意1:如果选择的机器人配置中没有使用 Inspire 系列灵巧手,那么请忽略本节内容。 > 注意1:如果选择的机器人配置中没有使用 Inspire 系列灵巧手,那么请忽略本节内容。
> >
> 注意2:如果选择的G1机器人配置,且使用 [Inspire DFX 灵巧手](https://support.unitree.com/home/zh/G1_developer/inspire_dfx_dexterous_hand),相关issue [#46](https://github.com/unitreerobotics/xr_teleoperate/issues/46)。 > 注意2:如果选择的G1机器人配置,且使用 [Inspire DFX 灵巧手](https://support.unitree.com/home/zh/G1_developer/inspire_dfx_dexterous_hand),相关issue [#46](https://github.com/unitreerobotics/xr_teleoperate/issues/46)。
> >
> 注意3:如果选择的机器人配置中使用了 [Inspire FTP 灵巧手](https://support.unitree.com/home/zh/G1_developer/inspire_ftp_dexterity_hand),相关issue [ #48](https://github.com/unitreerobotics/xr_teleoperate/issues/48)。
> 注意3:如果选择的机器人配置中使用了 [Inspire FTP 灵巧手](https://support.unitree.com/home/zh/G1_developer/inspire_ftp_dexterity_hand),相关issue [ #48](https://github.com/unitreerobotics/xr_teleoperate/issues/48)。目前已经支持 FTP 灵巧手,请您查阅 `--ee` 参数。
首先,使用 [此链接: DFX_inspire_service](https://github.com/unitreerobotics/DFX_inspire_service) 克隆灵巧手控制接口程序,然后将其复制到宇树机器人的**PC2**。 首先,使用 [此链接: DFX_inspire_service](https://github.com/unitreerobotics/DFX_inspire_service) 克隆灵巧手控制接口程序,然后将其复制到宇树机器人的**PC2**。
@ -396,8 +483,7 @@ sudo ./brainco_hand --id 127 --serial /dev/ttyUSB2
> >
> 1. 所有人员必须与机器人保持安全距离,以防止任何潜在的危险! > 1. 所有人员必须与机器人保持安全距离,以防止任何潜在的危险!
> 2. 在运行此程序之前,请确保至少阅读一次 [官方文档](https://support.unitree.com/home/zh/Teleoperation)。 > 2. 在运行此程序之前,请确保至少阅读一次 [官方文档](https://support.unitree.com/home/zh/Teleoperation)。
> 3. 没有开启**运动控制**模式(`--motion`)时,请务必确保机器人已经进入 [调试模式(L2+R2)](https://support.unitree.com/home/zh/G1_developer/remote_control),以停止运动控制程序发送指令,这样可以避免潜在的指令冲突问题。
> 4. 如果要开启**运动控制**模式遥操作,请提前使用 [R3遥控器](https://www.unitree.com/cn/R3) 确保机器人进入主运控模式。
> 3. 如果要开启**运动控制**模式遥操作,请提前使用 [R3遥控器](https://www.unitree.com/cn/R3) 确保机器人进入主运控模式。
> 5. 开启**运动控制**模式(`--motion`)时: > 5. 开启**运动控制**模式(`--motion`)时:
> - 右手柄按键 `A` 为遥操作**退出**功能按键; > - 右手柄按键 `A` 为遥操作**退出**功能按键;
> - 左手柄和右手柄的两个摇杆按键同时按下为软急停按键,机器人会退出运控程序并进入阻尼模式,该功能只在必要情况下使用 > - 左手柄和右手柄的两个摇杆按键同时按下为软急停按键,机器人会退出运控程序并进入阻尼模式,该功能只在必要情况下使用
@ -427,12 +513,8 @@ xr_teleoperate/
├── assets [存储机器人 URDF 相关文件] ├── assets [存储机器人 URDF 相关文件]
├── hardware [存储 3D 打印模组]
├── teleop ├── teleop
│ ├── image_server
│ │ ├── image_client.py [用于从机器人图像服务器接收图像数据]
│ │ ├── image_server.py [从摄像头捕获图像并通过网络发送(在机器人板载计算单元PC2上运行)]
│ ├── teleimager [全新的图像服务库,支持多种特性]
│ │ │ │
│ ├── televuer │ ├── televuer
│ │ ├── src/televuer │ │ ├── src/televuer
@ -454,6 +536,9 @@ xr_teleoperate/
│ │ ├── episode_writer.py [用于记录模仿学习的数据] │ │ ├── episode_writer.py [用于记录模仿学习的数据]
│ │ ├── weighted_moving_filter.py [用于过滤关节数据的滤波器] │ │ ├── weighted_moving_filter.py [用于过滤关节数据的滤波器]
│ │ ├── rerun_visualizer.py [用于可视化录制数据] │ │ ├── rerun_visualizer.py [用于可视化录制数据]
│ │ ├── ipc.py [用于和代理程序进行进程间通信]
│ │ ├── motion_switcher.py [用于切换运控状态]
│ │ ├── sim_state_topic.py [用于仿真部署]
│ │ │ │
│ │──teleop_hand_and_arm.py [遥操作的启动执行代码] │ │──teleop_hand_and_arm.py [遥操作的启动执行代码]
``` ```
@ -477,3 +562,4 @@ xr_teleoperate/
7. https://github.com/zeromq/pyzmq 7. https://github.com/zeromq/pyzmq
8. https://github.com/Dingry/BunnyVisionPro 8. https://github.com/Dingry/BunnyVisionPro
9. https://github.com/unitreerobotics/unitree_sdk2_python 9. https://github.com/unitreerobotics/unitree_sdk2_python
10. https://github.com/ARCLab-MIT/beavr-bot

197
teleop/image_server/image_client.py

@ -1,197 +0,0 @@
import cv2
import zmq
import numpy as np
import time
import struct
from collections import deque
from multiprocessing import shared_memory
import logging_mp
logger_mp = logging_mp.get_logger(__name__)
class ImageClient:
def __init__(self, tv_img_shape = None, tv_img_shm_name = None, wrist_img_shape = None, wrist_img_shm_name = None,
image_show = False, server_address = "192.168.123.164", port = 5555, Unit_Test = False):
"""
tv_img_shape: User's expected head camera resolution shape (H, W, C). It should match the output of the image service terminal.
tv_img_shm_name: Shared memory is used to easily transfer images across processes to the Vuer.
wrist_img_shape: User's expected wrist camera resolution shape (H, W, C). It should maintain the same shape as tv_img_shape.
wrist_img_shm_name: Shared memory is used to easily transfer images.
image_show: Whether to display received images in real time.
server_address: The ip address to execute the image server script.
port: The port number to bind to. It should be the same as the image server.
Unit_Test: When both server and client are True, it can be used to test the image transfer latency, \
network jitter, frame loss rate and other information.
"""
self.running = True
self._image_show = image_show
self._server_address = server_address
self._port = port
self.tv_img_shape = tv_img_shape
self.wrist_img_shape = wrist_img_shape
self.tv_enable_shm = False
if self.tv_img_shape is not None and tv_img_shm_name is not None:
self.tv_image_shm = shared_memory.SharedMemory(name=tv_img_shm_name)
self.tv_img_array = np.ndarray(tv_img_shape, dtype = np.uint8, buffer = self.tv_image_shm.buf)
self.tv_enable_shm = True
self.wrist_enable_shm = False
if self.wrist_img_shape is not None and wrist_img_shm_name is not None:
self.wrist_image_shm = shared_memory.SharedMemory(name=wrist_img_shm_name)
self.wrist_img_array = np.ndarray(wrist_img_shape, dtype = np.uint8, buffer = self.wrist_image_shm.buf)
self.wrist_enable_shm = True
# Performance evaluation parameters
self._enable_performance_eval = Unit_Test
if self._enable_performance_eval:
self._init_performance_metrics()
def _init_performance_metrics(self):
self._frame_count = 0 # Total frames received
self._last_frame_id = -1 # Last received frame ID
# Real-time FPS calculation using a time window
self._time_window = 1.0 # Time window size (in seconds)
self._frame_times = deque() # Timestamps of frames received within the time window
# Data transmission quality metrics
self._latencies = deque() # Latencies of frames within the time window
self._lost_frames = 0 # Total lost frames
self._total_frames = 0 # Expected total frames based on frame IDs
def _update_performance_metrics(self, timestamp, frame_id, receive_time):
# Update latency
latency = receive_time - timestamp
self._latencies.append(latency)
# Remove latencies outside the time window
while self._latencies and self._frame_times and self._latencies[0] < receive_time - self._time_window:
self._latencies.popleft()
# Update frame times
self._frame_times.append(receive_time)
# Remove timestamps outside the time window
while self._frame_times and self._frame_times[0] < receive_time - self._time_window:
self._frame_times.popleft()
# Update frame counts for lost frame calculation
expected_frame_id = self._last_frame_id + 1 if self._last_frame_id != -1 else frame_id
if frame_id != expected_frame_id:
lost = frame_id - expected_frame_id
if lost < 0:
logger_mp.info(f"[Image Client] Received out-of-order frame ID: {frame_id}")
else:
self._lost_frames += lost
logger_mp.warning(f"[Image Client] Detected lost frames: {lost}, Expected frame ID: {expected_frame_id}, Received frame ID: {frame_id}")
self._last_frame_id = frame_id
self._total_frames = frame_id + 1
self._frame_count += 1
def _print_performance_metrics(self, receive_time):
if self._frame_count % 30 == 0:
# Calculate real-time FPS
real_time_fps = len(self._frame_times) / self._time_window if self._time_window > 0 else 0
# Calculate latency metrics
if self._latencies:
avg_latency = sum(self._latencies) / len(self._latencies)
max_latency = max(self._latencies)
min_latency = min(self._latencies)
jitter = max_latency - min_latency
else:
avg_latency = max_latency = min_latency = jitter = 0
# Calculate lost frame rate
lost_frame_rate = (self._lost_frames / self._total_frames) * 100 if self._total_frames > 0 else 0
logger_mp.info(f"[Image Client] Real-time FPS: {real_time_fps:.2f}, Avg Latency: {avg_latency*1000:.2f} ms, Max Latency: {max_latency*1000:.2f} ms, \
Min Latency: {min_latency*1000:.2f} ms, Jitter: {jitter*1000:.2f} ms, Lost Frame Rate: {lost_frame_rate:.2f}%")
def _close(self):
self._socket.close()
self._context.term()
if self._image_show:
cv2.destroyAllWindows()
logger_mp.info("Image client has been closed.")
def receive_process(self):
# Set up ZeroMQ context and socket
self._context = zmq.Context()
self._socket = self._context.socket(zmq.SUB)
self._socket.connect(f"tcp://{self._server_address}:{self._port}")
self._socket.setsockopt_string(zmq.SUBSCRIBE, "")
logger_mp.info("Image client has started, waiting to receive data...")
try:
while self.running:
# Receive message
message = self._socket.recv()
receive_time = time.time()
if self._enable_performance_eval:
header_size = struct.calcsize('dI')
try:
# Attempt to extract header and image data
header = message[:header_size]
jpg_bytes = message[header_size:]
timestamp, frame_id = struct.unpack('dI', header)
except struct.error as e:
logger_mp.warning(f"[Image Client] Error unpacking header: {e}, discarding message.")
continue
else:
# No header, entire message is image data
jpg_bytes = message
# Decode image
np_img = np.frombuffer(jpg_bytes, dtype=np.uint8)
current_image = cv2.imdecode(np_img, cv2.IMREAD_COLOR)
if current_image is None:
logger_mp.warning("[Image Client] Failed to decode image.")
continue
if self.tv_enable_shm:
np.copyto(self.tv_img_array, np.array(current_image[:, :self.tv_img_shape[1]]))
if self.wrist_enable_shm:
np.copyto(self.wrist_img_array, np.array(current_image[:, -self.wrist_img_shape[1]:]))
if self._image_show:
height, width = current_image.shape[:2]
resized_image = cv2.resize(current_image, (width // 2, height // 2))
cv2.imshow('Image Client Stream', resized_image)
if cv2.waitKey(1) & 0xFF == ord('q'):
self.running = False
if self._enable_performance_eval:
self._update_performance_metrics(timestamp, frame_id, receive_time)
self._print_performance_metrics(receive_time)
except KeyboardInterrupt:
logger_mp.info("Image client interrupted by user.")
except Exception as e:
logger_mp.warning(f"[Image Client] An error occurred while receiving data: {e}")
finally:
self._close()
if __name__ == "__main__":
# example1
# tv_img_shape = (480, 1280, 3)
# img_shm = shared_memory.SharedMemory(create=True, size=np.prod(tv_img_shape) * np.uint8().itemsize)
# img_array = np.ndarray(tv_img_shape, dtype=np.uint8, buffer=img_shm.buf)
# img_client = ImageClient(tv_img_shape = tv_img_shape, tv_img_shm_name = img_shm.name)
# img_client.receive_process()
# example2
# Initialize the client with performance evaluation enabled
# client = ImageClient(image_show = True, server_address='127.0.0.1', Unit_Test=True) # local test
client = ImageClient(image_show = True, server_address='192.168.123.164', Unit_Test=False) # deployment test
client.receive_process()

321
teleop/image_server/image_server.py

@ -1,321 +0,0 @@
import cv2
import zmq
import time
import struct
from collections import deque
import numpy as np
import pyrealsense2 as rs
import logging_mp
logger_mp = logging_mp.get_logger(__name__, level=logging_mp.DEBUG)
class RealSenseCamera(object):
def __init__(self, img_shape, fps, serial_number=None, enable_depth=False) -> None:
"""
img_shape: [height, width]
serial_number: serial number
"""
self.img_shape = img_shape
self.fps = fps
self.serial_number = serial_number
self.enable_depth = enable_depth
align_to = rs.stream.color
self.align = rs.align(align_to)
self.init_realsense()
def init_realsense(self):
self.pipeline = rs.pipeline()
config = rs.config()
if self.serial_number is not None:
config.enable_device(self.serial_number)
config.enable_stream(rs.stream.color, self.img_shape[1], self.img_shape[0], rs.format.bgr8, self.fps)
if self.enable_depth:
config.enable_stream(rs.stream.depth, self.img_shape[1], self.img_shape[0], rs.format.z16, self.fps)
profile = self.pipeline.start(config)
self._device = profile.get_device()
if self._device is None:
logger_mp.error('[Image Server] pipe_profile.get_device() is None .')
if self.enable_depth:
assert self._device is not None
depth_sensor = self._device.first_depth_sensor()
self.g_depth_scale = depth_sensor.get_depth_scale()
self.intrinsics = profile.get_stream(rs.stream.color).as_video_stream_profile().get_intrinsics()
def get_frame(self):
frames = self.pipeline.wait_for_frames()
aligned_frames = self.align.process(frames)
color_frame = aligned_frames.get_color_frame()
if self.enable_depth:
depth_frame = aligned_frames.get_depth_frame()
if not color_frame:
return None
color_image = np.asanyarray(color_frame.get_data())
# color_image = cv2.cvtColor(color_image, cv2.COLOR_BGR2RGB)
depth_image = np.asanyarray(depth_frame.get_data()) if self.enable_depth else None
return color_image, depth_image
def release(self):
self.pipeline.stop()
class OpenCVCamera():
def __init__(self, device_id, img_shape, fps):
"""
decive_id: /dev/video* or *
img_shape: [height, width]
"""
self.id = device_id
self.fps = fps
self.img_shape = img_shape
self.cap = cv2.VideoCapture(self.id, cv2.CAP_V4L2)
self.cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('M', 'J', 'P', 'G'))
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.img_shape[0])
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.img_shape[1])
self.cap.set(cv2.CAP_PROP_FPS, self.fps)
# Test if the camera can read frames
if not self._can_read_frame():
logger_mp.error(f"[Image Server] Camera {self.id} Error: Failed to initialize the camera or read frames. Exiting...")
self.release()
def _can_read_frame(self):
success, _ = self.cap.read()
return success
def release(self):
self.cap.release()
def get_frame(self):
ret, color_image = self.cap.read()
if not ret:
return None
return color_image
class ImageServer:
def __init__(self, config, port = 5555, Unit_Test = False):
"""
config example1:
{
'fps':30 # frame per second
'head_camera_type': 'opencv', # opencv or realsense
'head_camera_image_shape': [480, 1280], # Head camera resolution [height, width]
'head_camera_id_numbers': [0], # '/dev/video0' (opencv)
'wrist_camera_type': 'realsense',
'wrist_camera_image_shape': [480, 640], # Wrist camera resolution [height, width]
'wrist_camera_id_numbers': ["218622271789", "241222076627"], # realsense camera's serial number
}
config example2:
{
'fps':30 # frame per second
'head_camera_type': 'realsense', # opencv or realsense
'head_camera_image_shape': [480, 640], # Head camera resolution [height, width]
'head_camera_id_numbers': ["218622271739"], # realsense camera's serial number
'wrist_camera_type': 'opencv',
'wrist_camera_image_shape': [480, 640], # Wrist camera resolution [height, width]
'wrist_camera_id_numbers': [0,1], # '/dev/video0' and '/dev/video1' (opencv)
}
If you are not using the wrist camera, you can comment out its configuration, like this below:
config:
{
'fps':30 # frame per second
'head_camera_type': 'opencv', # opencv or realsense
'head_camera_image_shape': [480, 1280], # Head camera resolution [height, width]
'head_camera_id_numbers': [0], # '/dev/video0' (opencv)
#'wrist_camera_type': 'realsense',
#'wrist_camera_image_shape': [480, 640], # Wrist camera resolution [height, width]
#'wrist_camera_id_numbers': ["218622271789", "241222076627"], # serial number (realsense)
}
"""
logger_mp.info(config)
self.fps = config.get('fps', 30)
self.head_camera_type = config.get('head_camera_type', 'opencv')
self.head_image_shape = config.get('head_camera_image_shape', [480, 640]) # (height, width)
self.head_camera_id_numbers = config.get('head_camera_id_numbers', [0])
self.wrist_camera_type = config.get('wrist_camera_type', None)
self.wrist_image_shape = config.get('wrist_camera_image_shape', [480, 640]) # (height, width)
self.wrist_camera_id_numbers = config.get('wrist_camera_id_numbers', None)
self.port = port
self.Unit_Test = Unit_Test
# Initialize head cameras
self.head_cameras = []
if self.head_camera_type == 'opencv':
for device_id in self.head_camera_id_numbers:
camera = OpenCVCamera(device_id=device_id, img_shape=self.head_image_shape, fps=self.fps)
self.head_cameras.append(camera)
elif self.head_camera_type == 'realsense':
for serial_number in self.head_camera_id_numbers:
camera = RealSenseCamera(img_shape=self.head_image_shape, fps=self.fps, serial_number=serial_number)
self.head_cameras.append(camera)
else:
logger_mp.warning(f"[Image Server] Unsupported head_camera_type: {self.head_camera_type}")
# Initialize wrist cameras if provided
self.wrist_cameras = []
if self.wrist_camera_type and self.wrist_camera_id_numbers:
if self.wrist_camera_type == 'opencv':
for device_id in self.wrist_camera_id_numbers:
camera = OpenCVCamera(device_id=device_id, img_shape=self.wrist_image_shape, fps=self.fps)
self.wrist_cameras.append(camera)
elif self.wrist_camera_type == 'realsense':
for serial_number in self.wrist_camera_id_numbers:
camera = RealSenseCamera(img_shape=self.wrist_image_shape, fps=self.fps, serial_number=serial_number)
self.wrist_cameras.append(camera)
else:
logger_mp.warning(f"[Image Server] Unsupported wrist_camera_type: {self.wrist_camera_type}")
# Set ZeroMQ context and socket
self.context = zmq.Context()
self.socket = self.context.socket(zmq.PUB)
self.socket.bind(f"tcp://*:{self.port}")
if self.Unit_Test:
self._init_performance_metrics()
for cam in self.head_cameras:
if isinstance(cam, OpenCVCamera):
logger_mp.info(f"[Image Server] Head camera {cam.id} resolution: {cam.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)} x {cam.cap.get(cv2.CAP_PROP_FRAME_WIDTH)}")
elif isinstance(cam, RealSenseCamera):
logger_mp.info(f"[Image Server] Head camera {cam.serial_number} resolution: {cam.img_shape[0]} x {cam.img_shape[1]}")
else:
logger_mp.warning("[Image Server] Unknown camera type in head_cameras.")
for cam in self.wrist_cameras:
if isinstance(cam, OpenCVCamera):
logger_mp.info(f"[Image Server] Wrist camera {cam.id} resolution: {cam.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)} x {cam.cap.get(cv2.CAP_PROP_FRAME_WIDTH)}")
elif isinstance(cam, RealSenseCamera):
logger_mp.info(f"[Image Server] Wrist camera {cam.serial_number} resolution: {cam.img_shape[0]} x {cam.img_shape[1]}")
else:
logger_mp.warning("[Image Server] Unknown camera type in wrist_cameras.")
logger_mp.info("[Image Server] Image server has started, waiting for client connections...")
def _init_performance_metrics(self):
self.frame_count = 0 # Total frames sent
self.time_window = 1.0 # Time window for FPS calculation (in seconds)
self.frame_times = deque() # Timestamps of frames sent within the time window
self.start_time = time.time() # Start time of the streaming
def _update_performance_metrics(self, current_time):
# Add current time to frame times deque
self.frame_times.append(current_time)
# Remove timestamps outside the time window
while self.frame_times and self.frame_times[0] < current_time - self.time_window:
self.frame_times.popleft()
# Increment frame count
self.frame_count += 1
def _print_performance_metrics(self, current_time):
if self.frame_count % 30 == 0:
elapsed_time = current_time - self.start_time
real_time_fps = len(self.frame_times) / self.time_window
logger_mp.info(f"[Image Server] Real-time FPS: {real_time_fps:.2f}, Total frames sent: {self.frame_count}, Elapsed time: {elapsed_time:.2f} sec")
def _close(self):
for cam in self.head_cameras:
cam.release()
for cam in self.wrist_cameras:
cam.release()
self.socket.close()
self.context.term()
logger_mp.info("[Image Server] The server has been closed.")
def send_process(self):
try:
while True:
head_frames = []
for cam in self.head_cameras:
if self.head_camera_type == 'opencv':
color_image = cam.get_frame()
if color_image is None:
logger_mp.error("[Image Server] Head camera frame read is error.")
break
elif self.head_camera_type == 'realsense':
color_image, depth_iamge = cam.get_frame()
if color_image is None:
logger_mp.error("[Image Server] Head camera frame read is error.")
break
head_frames.append(color_image)
if len(head_frames) != len(self.head_cameras):
break
head_color = cv2.hconcat(head_frames)
if self.wrist_cameras:
wrist_frames = []
for cam in self.wrist_cameras:
if self.wrist_camera_type == 'opencv':
color_image = cam.get_frame()
if color_image is None:
logger_mp.error("[Image Server] Wrist camera frame read is error.")
break
elif self.wrist_camera_type == 'realsense':
color_image, depth_iamge = cam.get_frame()
if color_image is None:
logger_mp.error("[Image Server] Wrist camera frame read is error.")
break
wrist_frames.append(color_image)
wrist_color = cv2.hconcat(wrist_frames)
# Concatenate head and wrist frames
full_color = cv2.hconcat([head_color, wrist_color])
else:
full_color = head_color
ret, buffer = cv2.imencode('.jpg', full_color)
if not ret:
logger_mp.error("[Image Server] Frame imencode is failed.")
continue
jpg_bytes = buffer.tobytes()
if self.Unit_Test:
timestamp = time.time()
frame_id = self.frame_count
header = struct.pack('dI', timestamp, frame_id) # 8-byte double, 4-byte unsigned int
message = header + jpg_bytes
else:
message = jpg_bytes
self.socket.send(message)
if self.Unit_Test:
current_time = time.time()
self._update_performance_metrics(current_time)
self._print_performance_metrics(current_time)
except KeyboardInterrupt:
logger_mp.warning("[Image Server] Interrupted by user.")
finally:
self._close()
if __name__ == "__main__":
config = {
'fps': 30,
'head_camera_type': 'opencv',
'head_camera_image_shape': [480, 1280], # Head camera resolution
'head_camera_id_numbers': [0],
'wrist_camera_type': 'opencv',
'wrist_camera_image_shape': [480, 640], # Wrist camera resolution
'wrist_camera_id_numbers': [2, 4],
}
server = ImageServer(config, Unit_Test=False)
server.send_process()

24
teleop/robot_control/robot_arm.py

@ -114,7 +114,7 @@ class G1_29_ArmController:
self.all_motor_q = self.get_current_motor_q() self.all_motor_q = self.get_current_motor_q()
logger_mp.debug(f"Current all body motor state q:\n{self.all_motor_q} \n") logger_mp.debug(f"Current all body motor state q:\n{self.all_motor_q} \n")
logger_mp.debug(f"Current two arms motor state q:\n{self.get_current_dual_arm_q()}\n") logger_mp.debug(f"Current two arms motor state q:\n{self.get_current_dual_arm_q()}\n")
logger_mp.info("Lock all joints except two arms...\n")
logger_mp.info("Lock all joints except two arms...")
arm_indices = set(member.value for member in G1_29_JointArmIndex) arm_indices = set(member.value for member in G1_29_JointArmIndex)
for id in G1_29_JointIndex: for id in G1_29_JointIndex:
@ -134,7 +134,7 @@ class G1_29_ArmController:
self.msg.motor_cmd[id].kp = self.kp_high self.msg.motor_cmd[id].kp = self.kp_high
self.msg.motor_cmd[id].kd = self.kd_high self.msg.motor_cmd[id].kd = self.kd_high
self.msg.motor_cmd[id].q = self.all_motor_q[id] self.msg.motor_cmd[id].q = self.all_motor_q[id]
logger_mp.info("Lock OK!\n")
logger_mp.info("Lock OK!")
# initialize publish thread # initialize publish thread
self.publish_thread = threading.Thread(target=self._ctrl_motor_state) self.publish_thread = threading.Thread(target=self._ctrl_motor_state)
@ -142,7 +142,7 @@ class G1_29_ArmController:
self.publish_thread.daemon = True self.publish_thread.daemon = True
self.publish_thread.start() self.publish_thread.start()
logger_mp.info("Initialize G1_29_ArmController OK!\n")
logger_mp.info("Initialize G1_29_ArmController OK!")
def _subscribe_motor_state(self): def _subscribe_motor_state(self):
while True: while True:
@ -402,7 +402,7 @@ class G1_23_ArmController:
self.all_motor_q = self.get_current_motor_q() self.all_motor_q = self.get_current_motor_q()
logger_mp.info(f"Current all body motor state q:\n{self.all_motor_q} \n") logger_mp.info(f"Current all body motor state q:\n{self.all_motor_q} \n")
logger_mp.info(f"Current two arms motor state q:\n{self.get_current_dual_arm_q()}\n") logger_mp.info(f"Current two arms motor state q:\n{self.get_current_dual_arm_q()}\n")
logger_mp.info("Lock all joints except two arms...\n")
logger_mp.info("Lock all joints except two arms...")
arm_indices = set(member.value for member in G1_23_JointArmIndex) arm_indices = set(member.value for member in G1_23_JointArmIndex)
for id in G1_23_JointIndex: for id in G1_23_JointIndex:
@ -422,7 +422,7 @@ class G1_23_ArmController:
self.msg.motor_cmd[id].kp = self.kp_high self.msg.motor_cmd[id].kp = self.kp_high
self.msg.motor_cmd[id].kd = self.kd_high self.msg.motor_cmd[id].kd = self.kd_high
self.msg.motor_cmd[id].q = self.all_motor_q[id] self.msg.motor_cmd[id].q = self.all_motor_q[id]
logger_mp.info("Lock OK!\n")
logger_mp.info("Lock OK!")
# initialize publish thread # initialize publish thread
self.publish_thread = threading.Thread(target=self._ctrl_motor_state) self.publish_thread = threading.Thread(target=self._ctrl_motor_state)
@ -430,7 +430,7 @@ class G1_23_ArmController:
self.publish_thread.daemon = True self.publish_thread.daemon = True
self.publish_thread.start() self.publish_thread.start()
logger_mp.info("Initialize G1_23_ArmController OK!\n")
logger_mp.info("Initialize G1_23_ArmController OK!")
def _subscribe_motor_state(self): def _subscribe_motor_state(self):
while True: while True:
@ -682,7 +682,7 @@ class H1_2_ArmController:
self.all_motor_q = self.get_current_motor_q() self.all_motor_q = self.get_current_motor_q()
logger_mp.info(f"Current all body motor state q:\n{self.all_motor_q} \n") logger_mp.info(f"Current all body motor state q:\n{self.all_motor_q} \n")
logger_mp.info(f"Current two arms motor state q:\n{self.get_current_dual_arm_q()}\n") logger_mp.info(f"Current two arms motor state q:\n{self.get_current_dual_arm_q()}\n")
logger_mp.info("Lock all joints except two arms...\n")
logger_mp.info("Lock all joints except two arms...")
arm_indices = set(member.value for member in H1_2_JointArmIndex) arm_indices = set(member.value for member in H1_2_JointArmIndex)
for id in H1_2_JointIndex: for id in H1_2_JointIndex:
@ -702,7 +702,7 @@ class H1_2_ArmController:
self.msg.motor_cmd[id].kp = self.kp_high self.msg.motor_cmd[id].kp = self.kp_high
self.msg.motor_cmd[id].kd = self.kd_high self.msg.motor_cmd[id].kd = self.kd_high
self.msg.motor_cmd[id].q = self.all_motor_q[id] self.msg.motor_cmd[id].q = self.all_motor_q[id]
logger_mp.info("Lock OK!\n")
logger_mp.info("Lock OK!")
# initialize publish thread # initialize publish thread
self.publish_thread = threading.Thread(target=self._ctrl_motor_state) self.publish_thread = threading.Thread(target=self._ctrl_motor_state)
@ -710,7 +710,7 @@ class H1_2_ArmController:
self.publish_thread.daemon = True self.publish_thread.daemon = True
self.publish_thread.start() self.publish_thread.start()
logger_mp.info("Initialize H1_2_ArmController OK!\n")
logger_mp.info("Initialize H1_2_ArmController OK!")
def _subscribe_motor_state(self): def _subscribe_motor_state(self):
while True: while True:
@ -964,7 +964,7 @@ class H1_ArmController:
self.all_motor_q = self.get_current_motor_q() self.all_motor_q = self.get_current_motor_q()
logger_mp.info(f"Current all body motor state q:\n{self.all_motor_q} \n") logger_mp.info(f"Current all body motor state q:\n{self.all_motor_q} \n")
logger_mp.info(f"Current two arms motor state q:\n{self.get_current_dual_arm_q()}\n") logger_mp.info(f"Current two arms motor state q:\n{self.get_current_dual_arm_q()}\n")
logger_mp.info("Lock all joints except two arms...\n")
logger_mp.info("Lock all joints except two arms...")
for id in H1_JointIndex: for id in H1_JointIndex:
if self._Is_weak_motor(id): if self._Is_weak_motor(id):
@ -976,7 +976,7 @@ class H1_ArmController:
self.msg.motor_cmd[id].kd = self.kd_high self.msg.motor_cmd[id].kd = self.kd_high
self.msg.motor_cmd[id].mode = 0x0A self.msg.motor_cmd[id].mode = 0x0A
self.msg.motor_cmd[id].q = self.all_motor_q[id] self.msg.motor_cmd[id].q = self.all_motor_q[id]
logger_mp.info("Lock OK!\n")
logger_mp.info("Lock OK!")
# initialize publish thread # initialize publish thread
self.publish_thread = threading.Thread(target=self._ctrl_motor_state) self.publish_thread = threading.Thread(target=self._ctrl_motor_state)
@ -984,7 +984,7 @@ class H1_ArmController:
self.publish_thread.daemon = True self.publish_thread.daemon = True
self.publish_thread.start() self.publish_thread.start()
logger_mp.info("Initialize H1_ArmController OK!\n")
logger_mp.info("Initialize H1_ArmController OK!")
def _subscribe_motor_state(self): def _subscribe_motor_state(self):
while True: while True:

92
teleop/robot_control/robot_arm_ik.py

@ -143,13 +143,22 @@ class G1_29_ArmIK:
self.opti.minimize(50 * self.translational_cost + self.rotation_cost + 0.02 * self.regularization_cost + 0.1 * self.smooth_cost) self.opti.minimize(50 * self.translational_cost + self.rotation_cost + 0.02 * self.regularization_cost + 0.1 * self.smooth_cost)
opts = { opts = {
'ipopt':{
'print_level':0,
'max_iter':50,
'tol':1e-6
},
'print_time':False,# print or not
'calc_lam_p':False # https://github.com/casadi/casadi/wiki/FAQ:-Why-am-I-getting-%22NaN-detected%22in-my-optimization%3F
# CasADi-level options
'expand': True,
'detect_simple_bounds': True,
'calc_lam_p': False, # https://github.com/casadi/casadi/wiki/FAQ:-Why-am-I-getting-%22NaN-detected%22in-my-optimization%3F
'print_time':False, # print or not
# IPOPT solver options
'ipopt.sb': 'yes', # disable Ipopt's license message
'ipopt.print_level': 0,
'ipopt.max_iter': 30,
'ipopt.tol': 1e-4,
'ipopt.acceptable_tol': 5e-4,
'ipopt.acceptable_iter': 5,
'ipopt.warm_start_init_point': 'yes',
'ipopt.derivative_test': 'none',
'ipopt.jacobian_approximation': 'exact',
# 'ipopt.hessian_approximation': 'limited-memory',
} }
self.opti.solver("ipopt", opts) self.opti.solver("ipopt", opts)
@ -369,13 +378,22 @@ class G1_23_ArmIK:
self.opti.minimize(50 * self.translational_cost + 0.5 * self.rotation_cost + 0.02 * self.regularization_cost + 0.1 * self.smooth_cost) self.opti.minimize(50 * self.translational_cost + 0.5 * self.rotation_cost + 0.02 * self.regularization_cost + 0.1 * self.smooth_cost)
opts = { opts = {
'ipopt':{
'print_level':0,
'max_iter':50,
'tol':1e-6
},
'print_time':False,# print or not
'calc_lam_p':False # https://github.com/casadi/casadi/wiki/FAQ:-Why-am-I-getting-%22NaN-detected%22in-my-optimization%3F
# CasADi-level options
'expand': True,
'detect_simple_bounds': True,
'calc_lam_p': False, # https://github.com/casadi/casadi/wiki/FAQ:-Why-am-I-getting-%22NaN-detected%22in-my-optimization%3F
'print_time':False, # print or not
# IPOPT solver options
'ipopt.sb': 'yes', # disable Ipopt's license message
'ipopt.print_level': 0,
'ipopt.max_iter': 30,
'ipopt.tol': 1e-4,
'ipopt.acceptable_tol': 5e-4,
'ipopt.acceptable_iter': 5,
'ipopt.warm_start_init_point': 'yes',
'ipopt.derivative_test': 'none',
'ipopt.jacobian_approximation': 'exact',
# 'ipopt.hessian_approximation': 'limited-memory',
} }
self.opti.solver("ipopt", opts) self.opti.solver("ipopt", opts)
@ -620,13 +638,22 @@ class H1_2_ArmIK:
self.opti.minimize(50 * self.translational_cost + self.rotation_cost + 0.02 * self.regularization_cost + 0.1 * self.smooth_cost) self.opti.minimize(50 * self.translational_cost + self.rotation_cost + 0.02 * self.regularization_cost + 0.1 * self.smooth_cost)
opts = { opts = {
'ipopt':{
'print_level':0,
'max_iter':50,
'tol':1e-6
},
'print_time':False,# print or not
'calc_lam_p':False # https://github.com/casadi/casadi/wiki/FAQ:-Why-am-I-getting-%22NaN-detected%22in-my-optimization%3F
# CasADi-level options
'expand': True,
'detect_simple_bounds': True,
'calc_lam_p': False, # https://github.com/casadi/casadi/wiki/FAQ:-Why-am-I-getting-%22NaN-detected%22in-my-optimization%3F
'print_time':False, # print or not
# IPOPT solver options
'ipopt.sb': 'yes', # disable Ipopt's license message
'ipopt.print_level': 0,
'ipopt.max_iter': 30,
'ipopt.tol': 1e-4,
'ipopt.acceptable_tol': 5e-4,
'ipopt.acceptable_iter': 5,
'ipopt.warm_start_init_point': 'yes',
'ipopt.derivative_test': 'none',
'ipopt.jacobian_approximation': 'exact',
# 'ipopt.hessian_approximation': 'limited-memory',
} }
self.opti.solver("ipopt", opts) self.opti.solver("ipopt", opts)
@ -874,13 +901,22 @@ class H1_ArmIK:
self.opti.minimize(50 * self.translational_cost + 0.5 * self.rotation_cost + 0.02 * self.regularization_cost + 0.1 * self.smooth_cost) self.opti.minimize(50 * self.translational_cost + 0.5 * self.rotation_cost + 0.02 * self.regularization_cost + 0.1 * self.smooth_cost)
opts = { opts = {
'ipopt':{
'print_level':0,
'max_iter':50,
'tol':1e-6
},
'print_time':False,# print or not
'calc_lam_p':False # https://github.com/casadi/casadi/wiki/FAQ:-Why-am-I-getting-%22NaN-detected%22in-my-optimization%3F
# CasADi-level options
'expand': True,
'detect_simple_bounds': True,
'calc_lam_p': False, # https://github.com/casadi/casadi/wiki/FAQ:-Why-am-I-getting-%22NaN-detected%22in-my-optimization%3F
'print_time':False, # print or not
# IPOPT solver options
'ipopt.sb': 'yes', # disable Ipopt's license message
'ipopt.print_level': 0,
'ipopt.max_iter': 30,
'ipopt.tol': 1e-4,
'ipopt.acceptable_tol': 5e-4,
'ipopt.acceptable_iter': 5,
'ipopt.warm_start_init_point': 'yes',
'ipopt.derivative_test': 'none',
'ipopt.jacobian_approximation': 'exact',
# 'ipopt.hessian_approximation': 'limited-memory',
} }
self.opti.solver("ipopt", opts) self.opti.solver("ipopt", opts)

2
teleop/robot_control/robot_hand_brainco.py

@ -67,7 +67,7 @@ class Brainco_Controller:
hand_control_process.daemon = True hand_control_process.daemon = True
hand_control_process.start() hand_control_process.start()
logger_mp.info("Initialize brainco_Controller OK!\n")
logger_mp.info("Initialize brainco_Controller OK!")
def _subscribe_hand_state(self): def _subscribe_hand_state(self):
while True: while True:

189
teleop/robot_control/robot_hand_inspire.py

@ -1,7 +1,6 @@
from unitree_sdk2py.core.channel import ChannelPublisher, ChannelSubscriber, ChannelFactoryInitialize # dds from unitree_sdk2py.core.channel import ChannelPublisher, ChannelSubscriber, ChannelFactoryInitialize # dds
from unitree_sdk2py.idl.unitree_go.msg.dds_ import MotorCmds_, MotorStates_ # idl from unitree_sdk2py.idl.unitree_go.msg.dds_ import MotorCmds_, MotorStates_ # idl
from unitree_sdk2py.idl.default import unitree_go_msg_dds__MotorCmd_ from unitree_sdk2py.idl.default import unitree_go_msg_dds__MotorCmd_
from teleop.robot_control.hand_retargeting import HandRetargeting, HandType from teleop.robot_control.hand_retargeting import HandRetargeting, HandType
import numpy as np import numpy as np
from enum import IntEnum from enum import IntEnum
@ -13,13 +12,13 @@ import logging_mp
logger_mp = logging_mp.get_logger(__name__) logger_mp = logging_mp.get_logger(__name__)
Inspire_Num_Motors = 6 Inspire_Num_Motors = 6
kTopicInspireCommand = "rt/inspire/cmd"
kTopicInspireState = "rt/inspire/state"
kTopicInspireDFXCommand = "rt/inspire/cmd"
kTopicInspireDFXState = "rt/inspire/state"
class Inspire_Controller:
class Inspire_Controller_DFX:
def __init__(self, left_hand_array, right_hand_array, dual_hand_data_lock = None, dual_hand_state_array = None, def __init__(self, left_hand_array, right_hand_array, dual_hand_data_lock = None, dual_hand_state_array = None,
dual_hand_action_array = None, fps = 100.0, Unit_Test = False, simulation_mode = False): dual_hand_action_array = None, fps = 100.0, Unit_Test = False, simulation_mode = False):
logger_mp.info("Initialize Inspire_Controller...")
logger_mp.info("Initialize Inspire_Controller_DFX...")
self.fps = fps self.fps = fps
self.Unit_Test = Unit_Test self.Unit_Test = Unit_Test
self.simulation_mode = simulation_mode self.simulation_mode = simulation_mode
@ -34,10 +33,10 @@ class Inspire_Controller:
ChannelFactoryInitialize(0) ChannelFactoryInitialize(0)
# initialize handcmd publisher and handstate subscriber # initialize handcmd publisher and handstate subscriber
self.HandCmb_publisher = ChannelPublisher(kTopicInspireCommand, MotorCmds_)
self.HandCmb_publisher = ChannelPublisher(kTopicInspireDFXCommand, MotorCmds_)
self.HandCmb_publisher.Init() self.HandCmb_publisher.Init()
self.HandState_subscriber = ChannelSubscriber(kTopicInspireState, MotorStates_)
self.HandState_subscriber = ChannelSubscriber(kTopicInspireDFXState, MotorStates_)
self.HandState_subscriber.Init() self.HandState_subscriber.Init()
# Shared Arrays for hand states # Shared Arrays for hand states
@ -53,15 +52,15 @@ class Inspire_Controller:
if any(self.right_hand_state_array): # any(self.left_hand_state_array) and if any(self.right_hand_state_array): # any(self.left_hand_state_array) and
break break
time.sleep(0.01) time.sleep(0.01)
logger_mp.warning("[Inspire_Controller] Waiting to subscribe dds...")
logger_mp.info("[Inspire_Controller] Subscribe dds ok.")
logger_mp.warning("[Inspire_Controller_DFX] Waiting to subscribe dds...")
logger_mp.info("[Inspire_Controller_DFX] Subscribe dds ok.")
hand_control_process = Process(target=self.control_process, args=(left_hand_array, right_hand_array, self.left_hand_state_array, self.right_hand_state_array, hand_control_process = Process(target=self.control_process, args=(left_hand_array, right_hand_array, self.left_hand_state_array, self.right_hand_state_array,
dual_hand_data_lock, dual_hand_state_array, dual_hand_action_array)) dual_hand_data_lock, dual_hand_state_array, dual_hand_action_array))
hand_control_process.daemon = True hand_control_process.daemon = True
hand_control_process.start() hand_control_process.start()
logger_mp.info("Initialize Inspire_Controller OK!\n")
logger_mp.info("Initialize Inspire_Controller_DFX OK!")
def _subscribe_hand_state(self): def _subscribe_hand_state(self):
while True: while True:
@ -154,9 +153,175 @@ class Inspire_Controller:
sleep_time = max(0, (1 / self.fps) - time_elapsed) sleep_time = max(0, (1 / self.fps) - time_elapsed)
time.sleep(sleep_time) time.sleep(sleep_time)
finally: finally:
logger_mp.info("Inspire_Controller has been closed.")
logger_mp.info("Inspire_Controller_DFX has been closed.")
kTopicInspireFTPLeftCommand = "rt/inspire_hand/ctrl/l"
kTopicInspireFTPRightCommand = "rt/inspire_hand/ctrl/r"
kTopicInspireFTPLeftState = "rt/inspire_hand/state/l"
kTopicInspireFTPRightState = "rt/inspire_hand/state/r"
class Inspire_Controller_FTP:
def __init__(self, left_hand_array, right_hand_array, dual_hand_data_lock = None, dual_hand_state_array = None,
dual_hand_action_array = None, fps = 100.0, Unit_Test = False, simulation_mode = False):
logger_mp.info("Initialize Inspire_Controller_FTP...")
from inspire_sdkpy import inspire_dds, inspire_hand_defaut # lazy import
self.fps = fps
self.Unit_Test = Unit_Test
self.simulation_mode = simulation_mode
if not self.Unit_Test:
self.hand_retargeting = HandRetargeting(HandType.INSPIRE_HAND)
else:
self.hand_retargeting = HandRetargeting(HandType.INSPIRE_HAND_Unit_Test)
if self.simulation_mode:
ChannelFactoryInitialize(1)
else:
ChannelFactoryInitialize(0)
# Initialize hand command publishers
self.LeftHandCmd_publisher = ChannelPublisher(kTopicInspireFTPLeftCommand, inspire_dds.inspire_hand_ctrl)
self.LeftHandCmd_publisher.Init()
self.RightHandCmd_publisher = ChannelPublisher(kTopicInspireFTPRightCommand, inspire_dds.inspire_hand_ctrl)
self.RightHandCmd_publisher.Init()
# Initialize hand state subscribers
self.LeftHandState_subscriber = ChannelSubscriber(kTopicInspireFTPLeftState, inspire_dds.inspire_hand_state)
self.LeftHandState_subscriber.Init() # Consider using callback if preferred: Init(callback_func, period_ms)
self.RightHandState_subscriber = ChannelSubscriber(kTopicInspireFTPRightState, inspire_dds.inspire_hand_state)
self.RightHandState_subscriber.Init()
# Shared Arrays for hand states ([0,1] normalized values)
self.left_hand_state_array = Array('d', Inspire_Num_Motors, lock=True)
self.right_hand_state_array = Array('d', Inspire_Num_Motors, lock=True)
# Initialize subscribe thread
self.subscribe_state_thread = threading.Thread(target=self._subscribe_hand_state)
self.subscribe_state_thread.daemon = True
self.subscribe_state_thread.start()
# Wait for initial DDS messages (optional, but good for ensuring connection)
wait_count = 0
while not (any(self.left_hand_state_array) or any(self.right_hand_state_array)):
if wait_count % 100 == 0: # Print every second
logger_mp.info(f"[Inspire_Controller_FTP] Waiting to subscribe to hand states from DDS (L: {any(self.left_hand_state_array)}, R: {any(self.right_hand_state_array)})...")
time.sleep(0.01)
wait_count += 1
if wait_count > 500: # Timeout after 5 seconds
logger_mp.warning("[Inspire_Controller_FTP] Warning: Timeout waiting for initial hand states. Proceeding anyway.")
break
logger_mp.info("[Inspire_Controller_FTP] Initial hand states received or timeout.")
hand_control_process = Process(target=self.control_process, args=(left_hand_array, right_hand_array, self.left_hand_state_array, self.right_hand_state_array,
dual_hand_data_lock, dual_hand_state_array, dual_hand_action_array))
hand_control_process.daemon = True
hand_control_process.start()
logger_mp.info("Initialize Inspire_Controller_FTP OK!\n")
def _subscribe_hand_state(self):
logger_mp.info("[Inspire_Controller_FTP] Subscribe thread started.")
while True:
# Left Hand
left_state_msg = self.LeftHandState_subscriber.Read()
if left_state_msg is not None:
if hasattr(left_state_msg, 'angle_act') and len(left_state_msg.angle_act) == Inspire_Num_Motors:
with self.left_hand_state_array.get_lock():
for i in range(Inspire_Num_Motors):
self.left_hand_state_array[i] = left_state_msg.angle_act[i] / 1000.0
else:
logger_mp.warning(f"[Inspire_Controller_FTP] Received left_state_msg but attributes are missing or incorrect. Type: {type(left_state_msg)}, Content: {str(left_state_msg)[:100]}")
# Right Hand
right_state_msg = self.RightHandState_subscriber.Read()
if right_state_msg is not None:
if hasattr(right_state_msg, 'angle_act') and len(right_state_msg.angle_act) == Inspire_Num_Motors:
with self.right_hand_state_array.get_lock():
for i in range(Inspire_Num_Motors):
self.right_hand_state_array[i] = right_state_msg.angle_act[i] / 1000.0
else:
logger_mp.warning(f"[Inspire_Controller_FTP] Received right_state_msg but attributes are missing or incorrect. Type: {type(right_state_msg)}, Content: {str(right_state_msg)[:100]}")
time.sleep(0.002)
def _send_hand_command(self, left_angle_cmd_scaled, right_angle_cmd_scaled):
"""
Send scaled angle commands [0-1000] to both hands.
"""
# Left Hand Command
left_cmd_msg = inspire_hand_defaut.get_inspire_hand_ctrl()
left_cmd_msg.angle_set = left_angle_cmd_scaled
left_cmd_msg.mode = 0b0001 # Mode 1: Angle control
self.LeftHandCmd_publisher.Write(left_cmd_msg)
# Right Hand Command
right_cmd_msg = inspire_hand_defaut.get_inspire_hand_ctrl()
right_cmd_msg.angle_set = right_angle_cmd_scaled
right_cmd_msg.mode = 0b0001 # Mode 1: Angle control
self.RightHandCmd_publisher.Write(right_cmd_msg)
def control_process(self, left_hand_array, right_hand_array, left_hand_state_array, right_hand_state_array,
dual_hand_data_lock = None, dual_hand_state_array = None, dual_hand_action_array = None):
logger_mp.info("[Inspire_Controller_FTP] Control process started.")
self.running = True
left_q_target = np.full(Inspire_Num_Motors, 1.0)
right_q_target = np.full(Inspire_Num_Motors, 1.0)
try:
while self.running:
start_time = time.time()
# get dual hand state
with left_hand_array.get_lock():
left_hand_data = np.array(left_hand_array[:]).reshape(25, 3).copy()
with right_hand_array.get_lock():
right_hand_data = np.array(right_hand_array[:]).reshape(25, 3).copy()
# Read left and right q_state from shared arrays
state_data = np.concatenate((np.array(left_hand_state_array[:]), np.array(right_hand_state_array[:])))
if not np.all(right_hand_data == 0.0) and not np.all(left_hand_data[4] == np.array([-1.13, 0.3, 0.15])): # if hand data has been initialized.
ref_left_value = left_hand_data[self.hand_retargeting.left_indices[1,:]] - left_hand_data[self.hand_retargeting.left_indices[0,:]]
ref_right_value = right_hand_data[self.hand_retargeting.right_indices[1,:]] - right_hand_data[self.hand_retargeting.right_indices[0,:]]
left_q_target = self.hand_retargeting.left_retargeting.retarget(ref_left_value)[self.hand_retargeting.left_dex_retargeting_to_hardware]
right_q_target = self.hand_retargeting.right_retargeting.retarget(ref_right_value)[self.hand_retargeting.right_dex_retargeting_to_hardware]
def normalize(val, min_val, max_val):
return np.clip((max_val - val) / (max_val - min_val), 0.0, 1.0)
for idx in range(Inspire_Num_Motors):
if idx <= 3:
left_q_target[idx] = normalize(left_q_target[idx], 0.0, 1.7)
right_q_target[idx] = normalize(right_q_target[idx], 0.0, 1.7)
elif idx == 4:
left_q_target[idx] = normalize(left_q_target[idx], 0.0, 0.5)
right_q_target[idx] = normalize(right_q_target[idx], 0.0, 0.5)
elif idx == 5:
left_q_target[idx] = normalize(left_q_target[idx], -0.1, 1.3)
right_q_target[idx] = normalize(right_q_target[idx], -0.1, 1.3)
scaled_left_cmd = [int(np.clip(val * 1000, 0, 1000)) for val in left_q_target]
scaled_right_cmd = [int(np.clip(val * 1000, 0, 1000)) for val in right_q_target]
# get dual hand action
action_data = np.concatenate((left_q_target, right_q_target))
if dual_hand_state_array and dual_hand_action_array:
with dual_hand_data_lock:
dual_hand_state_array[:] = state_data
dual_hand_action_array[:] = action_data
self._send_hand_command(scaled_left_cmd, scaled_right_cmd)
current_time = time.time()
time_elapsed = current_time - start_time
sleep_time = max(0, (1 / self.fps) - time_elapsed)
time.sleep(sleep_time)
finally:
logger_mp.info("Inspire_Controller_FTP has been closed.")
# Update hand state, according to the official documentation, https://support.unitree.com/home/en/G1_developer/inspire_dfx_dexterous_hand
# Update hand state, according to the official documentation:
# 1. https://support.unitree.com/home/en/G1_developer/inspire_dfx_dexterous_hand
# 2. https://support.unitree.com/home/en/G1_developer/inspire_ftp_dexterity_hand
# the state sequence is as shown in the table below # the state sequence is as shown in the table below
# ┌──────┬───────┬──────┬────────┬────────┬────────────┬────────────────┬───────┬──────┬────────┬────────┬────────────┬────────────────┐ # ┌──────┬───────┬──────┬────────┬────────┬────────────┬────────────────┬───────┬──────┬────────┬────────┬────────────┬────────────────┐
# │ Id │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ # │ Id │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │

53
teleop/robot_control/robot_hand_unitree.py

@ -13,7 +13,7 @@ import time
import os import os
import sys import sys
import threading import threading
from multiprocessing import Process, shared_memory, Array, Value, Lock
from multiprocessing import Process, Array, Value, Lock
parent2_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) parent2_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(parent2_dir) sys.path.append(parent2_dir)
@ -100,7 +100,7 @@ class Dex3_1_Controller:
hand_control_process.daemon = True hand_control_process.daemon = True
hand_control_process.start() hand_control_process.start()
logger_mp.info("Initialize Dex3_1_Controller OK!\n")
logger_mp.info("Initialize Dex3_1_Controller OK!")
def _subscribe_hand_state(self): def _subscribe_hand_state(self):
while True: while True:
@ -304,7 +304,7 @@ class Dex1_1_Gripper_Controller:
self.gripper_control_thread.daemon = True self.gripper_control_thread.daemon = True
self.gripper_control_thread.start() self.gripper_control_thread.start()
logger_mp.info("Initialize Dex1_1_Gripper_Controller OK!\n")
logger_mp.info("Initialize Dex1_1_Gripper_Controller OK!")
def _subscribe_gripper_state(self): def _subscribe_gripper_state(self):
while True: while True:
@ -406,7 +406,7 @@ class Gripper_JointIndex(IntEnum):
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
from televuer import TeleVuerWrapper from televuer import TeleVuerWrapper
from teleop.image_server.image_client import ImageClient
from teleimager import ImageClient
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--xr-mode', type=str, choices=['hand', 'controller'], default='hand', help='Select XR device tracking source') parser.add_argument('--xr-mode', type=str, choices=['hand', 'controller'], default='hand', help='Select XR device tracking source')
@ -414,34 +414,15 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
logger_mp.info(f"args:{args}\n") logger_mp.info(f"args:{args}\n")
# image
img_config = {
'fps': 30,
'head_camera_type': 'opencv',
'head_camera_image_shape': [480, 1280], # Head camera resolution
'head_camera_id_numbers': [0],
}
ASPECT_RATIO_THRESHOLD = 2.0 # If the aspect ratio exceeds this value, it is considered binocular
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 = True
else:
BINOCULAR = False
# image
if 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)
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)
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 = True
image_receive_thread.start()
# image client
img_client = ImageClient(host='127.0.0.1') #host='192.168.123.164'
if not img_client.has_head_cam():
logger_mp.error("Head camera is required. Please enable head camera on the image server side.")
head_img_shape = img_client.get_head_shape()
tv_binocular = img_client.head_is_binocular()
# television: obtain hand pose data from the XR device and transmit the robot's head camera image to the XR device. # television: obtain hand pose data from the XR device and transmit the robot's head camera image to the XR device.
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)
tv_wrapper = TeleVuerWrapper(binocular=tv_binocular, use_hand_tracking=args.xr_mode == "hand", img_shape=head_img_shape, return_hand_rot_data = False)
# end-effector # end-effector
if args.ee == "dex3": if args.ee == "dex3":
@ -462,7 +443,9 @@ if __name__ == "__main__":
user_input = input("Please enter the start signal (enter 's' to start the subsequent program):\n") user_input = input("Please enter the start signal (enter 's' to start the subsequent program):\n")
if user_input.lower() == 's': if user_input.lower() == 's':
while True: while True:
tele_data = tv_wrapper.get_motion_state_data()
head_img, head_img_fps = img_client.get_head_frame()
tv_wrapper.set_display_image(head_img)
tele_data = tv_wrapper.get_tele_data()
if args.ee == "dex3" and args.xr_mode == "hand": if args.ee == "dex3" and args.xr_mode == "hand":
with left_hand_pos_array.get_lock(): with left_hand_pos_array.get_lock():
left_hand_pos_array[:] = tele_data.left_hand_pos.flatten() left_hand_pos_array[:] = tele_data.left_hand_pos.flatten()
@ -470,14 +453,14 @@ if __name__ == "__main__":
right_hand_pos_array[:] = tele_data.right_hand_pos.flatten() right_hand_pos_array[:] = tele_data.right_hand_pos.flatten()
elif args.ee == "dex1" and args.xr_mode == "controller": elif args.ee == "dex1" and args.xr_mode == "controller":
with left_gripper_value.get_lock(): with left_gripper_value.get_lock():
left_gripper_value.value = tele_data.left_trigger_value
left_gripper_value.value = tele_data.left_ctrl_triggerValue
with right_gripper_value.get_lock(): with right_gripper_value.get_lock():
right_gripper_value.value = tele_data.right_trigger_value
right_gripper_value.value = tele_data.right_ctrl_triggerValue
elif args.ee == "dex1" and args.xr_mode == "hand": elif args.ee == "dex1" and args.xr_mode == "hand":
with left_gripper_value.get_lock(): with left_gripper_value.get_lock():
left_gripper_value.value = tele_data.left_pinch_value
left_gripper_value.value = tele_data.left_hand_pinchValue
with right_gripper_value.get_lock(): with right_gripper_value.get_lock():
right_gripper_value.value = tele_data.right_pinch_value
right_gripper_value.value = tele_data.right_hand_pinchValue
else: else:
pass pass

1
teleop/teleimager

@ -0,0 +1 @@
Subproject commit 81720e0bb384d9f79cb10160e9e604d505f1e1a2

376
teleop/teleop_hand_and_arm.py

@ -1,8 +1,6 @@
import numpy as np
import time import time
import argparse import argparse
import cv2
from multiprocessing import shared_memory, Value, Array, Lock
from multiprocessing import Value, Array, Lock
import threading import threading
import logging_mp import logging_mp
logging_mp.basic_config(level=logging_mp.INFO) logging_mp.basic_config(level=logging_mp.INFO)
@ -18,17 +16,18 @@ from televuer import TeleVuerWrapper
from teleop.robot_control.robot_arm import G1_29_ArmController, G1_23_ArmController, H1_2_ArmController, H1_ArmController 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_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, Dex1_1_Gripper_Controller from teleop.robot_control.robot_hand_unitree import Dex3_1_Controller, Dex1_1_Gripper_Controller
from teleop.robot_control.robot_hand_inspire import Inspire_Controller
from teleop.robot_control.robot_hand_inspire import Inspire_Controller_DFX, Inspire_Controller_FTP
from teleop.robot_control.robot_hand_brainco import Brainco_Controller from teleop.robot_control.robot_hand_brainco import Brainco_Controller
from teleop.image_server.image_client import ImageClient
from teleimager.image_client import ImageClient
from teleop.utils.episode_writer import EpisodeWriter from teleop.utils.episode_writer import EpisodeWriter
from teleop.utils.ipc import IPC_Server from teleop.utils.ipc import IPC_Server
from teleop.utils.motion_switcher import MotionSwitcher, LocoClientWrapper
from sshkeyboard import listen_keyboard, stop_listening from sshkeyboard import listen_keyboard, stop_listening
# for simulation # for simulation
from unitree_sdk2py.core.channel import ChannelPublisher from unitree_sdk2py.core.channel import ChannelPublisher
from unitree_sdk2py.idl.std_msgs.msg.dds_ import String_ from unitree_sdk2py.idl.std_msgs.msg.dds_ import String_
def publish_reset_category(category: int,publisher): # Scene Reset signal
def publish_reset_category(category: int, publisher): # Scene Reset signal
msg = String_(data=str(category)) msg = String_(data=str(category))
publisher.Write(msg) publisher.Write(msg)
logger_mp.info(f"published reset category: {category}") logger_mp.info(f"published reset category: {category}")
@ -36,13 +35,21 @@ def publish_reset_category(category: int,publisher): # Scene Reset signal
# state transition # state transition
START = False # Enable to start robot following VR user motion START = False # Enable to start robot following VR user motion
STOP = False # Enable to begin system exit procedure STOP = False # Enable to begin system exit procedure
RECORD_TOGGLE = False # [Ready] ⇄ [Recording] ⟶ [AutoSave] ⟶ [Ready] (⇄ manual) (⟶ auto)
READY = False # Ready to (1) enter START state, (2) enter RECORD_RUNNING state
RECORD_RUNNING = False # True if [Recording] RECORD_RUNNING = False # True if [Recording]
RECORD_READY = True # True if [Ready], False if [Recording] / [AutoSave]
# task info
TASK_NAME = None
TASK_DESC = None
ITEM_ID = None
RECORD_TOGGLE = False # Toggle recording state
# ------- --------- ----------- ----------- ---------
# state [Ready] ==> [Recording] ==> [AutoSave] --> [Ready]
# ------- --------- | ----------- | ----------- | ---------
# START True |manual True |manual True | True
# READY True |set False |set False |auto True
# RECORD_RUNNING False |to True |to False | False
# ∨ ∨ ∨
# RECORD_TOGGLE False True False True False False
# ------- --------- ----------- ----------- ---------
# ==> manual: when READY is True, set RECORD_TOGGLE=True to transition.
# --> auto : Auto-transition after saving data.
def on_press(key): def on_press(key):
global STOP, START, RECORD_TOGGLE global STOP, START, RECORD_TOGGLE
if key == 'r': if key == 'r':
@ -55,119 +62,81 @@ def on_press(key):
else: else:
logger_mp.warning(f"[on_press] {key} was pressed, but no action is defined for this key.") logger_mp.warning(f"[on_press] {key} was pressed, but no action is defined for this key.")
def on_info(info):
"""Only handle CMD_TOGGLE_RECORD's task info"""
global TASK_NAME, TASK_DESC, ITEM_ID
TASK_NAME = info.get("task_name")
TASK_DESC = info.get("task_desc")
ITEM_ID = info.get("item_id")
logger_mp.debug(f"[on_info] Updated globals: {TASK_NAME}, {TASK_DESC}, {ITEM_ID}")
def get_state() -> dict: def get_state() -> dict:
"""Return current heartbeat state""" """Return current heartbeat state"""
global START, STOP, RECORD_RUNNING, RECORD_READY
global START, STOP, RECORD_RUNNING, READY
return { return {
"START": START, "START": START,
"STOP": STOP, "STOP": STOP,
"READY": READY,
"RECORD_RUNNING": RECORD_RUNNING, "RECORD_RUNNING": RECORD_RUNNING,
"RECORD_READY": RECORD_READY,
} }
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--frequency', type = float, default = 30.0, help = 'save data\'s frequency')
# basic control parameters # basic control parameters
parser.add_argument('--xr-mode', type=str, choices=['hand', 'controller'], default='hand', help='Select XR device tracking source')
parser.add_argument('--frequency', type = float, default = 30.0, help = 'control and record \'s frequency')
parser.add_argument('--input-mode', type=str, choices=['hand', 'controller'], default='hand', help='Select XR device input tracking source')
parser.add_argument('--display-mode', type=str, choices=['immersive', 'ego', 'pass-through'], default='immersive', help='Select XR device display mode')
parser.add_argument('--arm', type=str, choices=['G1_29', 'G1_23', 'H1_2', 'H1'], default='G1_29', help='Select arm controller') 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=['dex1', 'dex3', 'inspire1', 'brainco'], help='Select end effector controller')
parser.add_argument('--ee', type=str, choices=['dex1', 'dex3', 'inspire_ftp', 'inspire_dfx', 'brainco'], help='Select end effector controller')
parser.add_argument('--img-server-ip', type=str, default='192.168.123.164', help='IP address of image server, used by teleimager and televuer')
# mode flags # mode flags
parser.add_argument('--motion', action = 'store_true', help = 'Enable motion control mode') 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('--headless', action='store_true', help='Enable headless mode (no display)')
parser.add_argument('--sim', action = 'store_true', help = 'Enable isaac simulation mode') parser.add_argument('--sim', action = 'store_true', help = 'Enable isaac simulation mode')
parser.add_argument('--affinity', action = 'store_true', help = 'Enable high priority and set CPU affinity')
parser.add_argument('--ipc', action = 'store_true', help = 'Enable IPC server to handle input; otherwise enable sshkeyboard') parser.add_argument('--ipc', action = 'store_true', help = 'Enable IPC server to handle input; otherwise enable sshkeyboard')
parser.add_argument('--record', action = 'store_true', help = 'Enable data recording')
parser.add_argument('--affinity', action = 'store_true', help = 'Enable high priority and set CPU affinity mode')
# record mode and task info
parser.add_argument('--record', action = 'store_true', help = 'Enable data recording mode')
parser.add_argument('--task-dir', type = str, default = './utils/data/', help = 'path to save data') parser.add_argument('--task-dir', type = str, default = './utils/data/', help = 'path to save data')
parser.add_argument('--task-name', type = str, default = 'pick cube', help = 'task name for recording')
parser.add_argument('--task-desc', type = str, default = 'e.g. pick the red cube on the table.', help = 'task goal for recording')
parser.add_argument('--task-name', type = str, default = 'pick cube', help = 'task file name for recording')
parser.add_argument('--task-goal', type = str, default = 'pick up cube.', help = 'task goal for recording at json file')
parser.add_argument('--task-desc', type = str, default = 'task description', help = 'task description for recording at json file')
parser.add_argument('--task-steps', type = str, default = 'step1: do this; step2: do that;', help = 'task steps for recording at json file')
args = parser.parse_args() args = parser.parse_args()
logger_mp.info(f"args: {args}") logger_mp.info(f"args: {args}")
try: try:
# ipc communication. client usage: see utils/ipc.py
# ipc communication mode. client usage: see utils/ipc.py
if args.ipc: if args.ipc:
ipc_server = IPC_Server(on_press=on_press, on_info=on_info, get_state=get_state)
ipc_server = IPC_Server(on_press=on_press,get_state=get_state)
ipc_server.start() ipc_server.start()
# sshkeyboard communication
# sshkeyboard communication mode
else: else:
listen_keyboard_thread = threading.Thread(target=listen_keyboard, kwargs={"on_press": on_press, "until": None, "sequential": False,}, daemon=True)
listen_keyboard_thread = threading.Thread(target=listen_keyboard,
kwargs={"on_press": on_press, "until": None, "sequential": False,},
daemon=True)
listen_keyboard_thread.start() listen_keyboard_thread.start()
# image client: img_config should be the same as the configuration in image_server.py (of Robot's development computing unit)
if args.sim:
img_config = {
'fps': 30,
'head_camera_type': 'opencv',
'head_camera_image_shape': [480, 640], # Head camera resolution
'head_camera_id_numbers': [0],
'wrist_camera_type': 'opencv',
'wrist_camera_image_shape': [480, 640], # Wrist camera resolution
'wrist_camera_id_numbers': [2, 4],
}
else:
img_config = {
'fps': 30,
'head_camera_type': 'opencv',
'head_camera_image_shape': [480, 1280], # Head camera resolution
'head_camera_id_numbers': [0],
'wrist_camera_type': 'opencv',
'wrist_camera_image_shape': [480, 640], # Wrist camera resolution
'wrist_camera_id_numbers': [2, 4],
}
ASPECT_RATIO_THRESHOLD = 2.0 # If the aspect ratio exceeds this value, it is considered binocular
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 = True
else:
BINOCULAR = False
if 'wrist_camera_type' in img_config:
WRIST = True
else:
WRIST = False
if 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)
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)
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)
# image client
img_client = ImageClient(host=args.img_server_ip)
camera_config = img_client.get_cam_config()
logger_mp.debug(f"Camera config: {camera_config}")
xr_need_local_img = not (args.display_mode == 'pass-through' or camera_config['head_camera']['enable_webrtc'])
# televuer_wrapper: obtain hand pose data from the XR device and transmit the robot's head camera image to the XR device.
tv_wrapper = TeleVuerWrapper(use_hand_tracking=args.input_mode == "hand",
binocular=camera_config['head_camera']['binocular'],
img_shape=camera_config['head_camera']['image_shape'],
# maybe should decrease fps for better performance?
# https://github.com/unitreerobotics/xr_teleoperate/issues/172
# display_fps=camera_config['head_camera']['fps'] ? args.frequency? 30.0?
display_mode=args.display_mode,
zmq=camera_config['head_camera']['enable_zmq'],
webrtc=camera_config['head_camera']['enable_webrtc'],
webrtc_url=f"https://{args.img_server_ip}:{camera_config['head_camera']['webrtc_port']}/offer",
)
# motion mode (G1: Regular mode R1+X, not Running mode R2+A)
if args.motion:
if args.input_mode == "controller":
loco_wrapper = LocoClientWrapper()
else: 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 = True
image_receive_thread.start()
# television: obtain hand pose data from the XR device and transmit the robot's head camera image to the XR device.
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)
motion_switcher = MotionSwitcher()
status, result = motion_switcher.Enter_Debug_Mode()
logger_mp.info(f"Enter debug mode: {'Success' if status == 0 else 'Failed'}")
# arm # arm
if args.arm == "G1_29": if args.arm == "G1_29":
@ -190,28 +159,38 @@ if __name__ == '__main__':
dual_hand_data_lock = Lock() dual_hand_data_lock = Lock()
dual_hand_state_array = Array('d', 14, lock = False) # [output] current left, right hand state(14) data. dual_hand_state_array = Array('d', 14, lock = False) # [output] current left, right hand state(14) data.
dual_hand_action_array = Array('d', 14, lock = False) # [output] current left, right hand action(14) data. dual_hand_action_array = Array('d', 14, lock = False) # [output] current left, right hand action(14) data.
hand_ctrl = Dex3_1_Controller(left_hand_pos_array, right_hand_pos_array, dual_hand_data_lock, dual_hand_state_array, dual_hand_action_array, simulation_mode=args.sim)
hand_ctrl = Dex3_1_Controller(left_hand_pos_array, right_hand_pos_array, dual_hand_data_lock,
dual_hand_state_array, dual_hand_action_array, simulation_mode=args.sim)
elif args.ee == "dex1": elif args.ee == "dex1":
left_gripper_value = Value('d', 0.0, lock=True) # [input] left_gripper_value = Value('d', 0.0, lock=True) # [input]
right_gripper_value = Value('d', 0.0, lock=True) # [input] right_gripper_value = Value('d', 0.0, lock=True) # [input]
dual_gripper_data_lock = Lock() dual_gripper_data_lock = Lock()
dual_gripper_state_array = Array('d', 2, lock=False) # current left, right gripper state(2) data. dual_gripper_state_array = Array('d', 2, lock=False) # current left, right gripper state(2) data.
dual_gripper_action_array = Array('d', 2, lock=False) # current left, right gripper action(2) data. dual_gripper_action_array = Array('d', 2, lock=False) # current left, right gripper action(2) data.
gripper_ctrl = Dex1_1_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":
gripper_ctrl = Dex1_1_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 == "inspire_dfx":
left_hand_pos_array = Array('d', 75, lock = True) # [input]
right_hand_pos_array = Array('d', 75, lock = True) # [input]
dual_hand_data_lock = Lock()
dual_hand_state_array = Array('d', 12, lock = False) # [output] current left, right hand state(12) data.
dual_hand_action_array = Array('d', 12, lock = False) # [output] current left, right hand action(12) data.
hand_ctrl = Inspire_Controller_DFX(left_hand_pos_array, right_hand_pos_array, dual_hand_data_lock, dual_hand_state_array, dual_hand_action_array, simulation_mode=args.sim)
elif args.ee == "inspire_ftp":
left_hand_pos_array = Array('d', 75, lock = True) # [input] left_hand_pos_array = Array('d', 75, lock = True) # [input]
right_hand_pos_array = Array('d', 75, lock = True) # [input] right_hand_pos_array = Array('d', 75, lock = True) # [input]
dual_hand_data_lock = Lock() dual_hand_data_lock = Lock()
dual_hand_state_array = Array('d', 12, lock = False) # [output] current left, right hand state(12) data. dual_hand_state_array = Array('d', 12, lock = False) # [output] current left, right hand state(12) data.
dual_hand_action_array = Array('d', 12, lock = False) # [output] current left, right hand action(12) data. dual_hand_action_array = Array('d', 12, lock = False) # [output] current left, right hand action(12) data.
hand_ctrl = Inspire_Controller(left_hand_pos_array, right_hand_pos_array, dual_hand_data_lock, dual_hand_state_array, dual_hand_action_array, simulation_mode=args.sim)
hand_ctrl = Inspire_Controller_FTP(left_hand_pos_array, right_hand_pos_array, dual_hand_data_lock, dual_hand_state_array, dual_hand_action_array, simulation_mode=args.sim)
elif args.ee == "brainco": elif args.ee == "brainco":
left_hand_pos_array = Array('d', 75, lock = True) # [input] left_hand_pos_array = Array('d', 75, lock = True) # [input]
right_hand_pos_array = Array('d', 75, lock = True) # [input] right_hand_pos_array = Array('d', 75, lock = True) # [input]
dual_hand_data_lock = Lock() dual_hand_data_lock = Lock()
dual_hand_state_array = Array('d', 12, lock = False) # [output] current left, right hand state(12) data. dual_hand_state_array = Array('d', 12, lock = False) # [output] current left, right hand state(12) data.
dual_hand_action_array = Array('d', 12, lock = False) # [output] current left, right hand action(12) data. dual_hand_action_array = Array('d', 12, lock = False) # [output] current left, right hand action(12) data.
hand_ctrl = Brainco_Controller(left_hand_pos_array, right_hand_pos_array, dual_hand_data_lock, dual_hand_state_array, dual_hand_action_array, simulation_mode=args.sim)
hand_ctrl = Brainco_Controller(left_hand_pos_array, right_hand_pos_array, dual_hand_data_lock,
dual_hand_state_array, dual_hand_action_array, simulation_mode=args.sim)
else: else:
pass pass
@ -241,44 +220,42 @@ if __name__ == '__main__':
from teleop.utils.sim_state_topic import start_sim_state_subscribe from teleop.utils.sim_state_topic import start_sim_state_subscribe
sim_state_subscriber = start_sim_state_subscribe() sim_state_subscriber = start_sim_state_subscribe()
# controller + motion mode
if args.xr_mode == "controller" and args.motion:
from unitree_sdk2py.g1.loco.g1_loco_client import LocoClient
sport_client = LocoClient()
sport_client.SetTimeout(0.0001)
sport_client.Init()
# record + headless mode
if args.record and args.headless:
recorder = EpisodeWriter(task_dir = args.task_dir + args.task_name, task_goal = args.task_desc, frequency = args.frequency, rerun_log = False)
elif args.record and not args.headless:
recorder = EpisodeWriter(task_dir = args.task_dir + args.task_name, task_goal = args.task_desc, frequency = args.frequency, rerun_log = True)
# record + headless / non-headless mode
if args.record:
recorder = EpisodeWriter(task_dir = os.path.join(args.task_dir, args.task_name),
task_goal = args.task_goal,
task_desc = args.task_desc,
task_steps = args.task_steps,
frequency = args.frequency,
rerun_log = not args.headless)
logger_mp.info("Please enter the start signal (enter 'r' to start the subsequent program)") logger_mp.info("Please enter the start signal (enter 'r' to start the subsequent program)")
while not START and not STOP:
time.sleep(0.01)
logger_mp.info("start program.")
READY = True # now ready to (1) enter START state
while not START and not STOP: # wait for start or stop signal.
time.sleep(0.033)
if camera_config['head_camera']['enable_zmq'] and xr_need_local_img:
head_img, _ = img_client.get_head_frame()
tv_wrapper.render_to_xr(head_img)
logger_mp.info("---------------------🚀start program🚀-------------------------")
arm_ctrl.speed_gradual_max() arm_ctrl.speed_gradual_max()
# main loop. robot start to follow VR user's motion
while not STOP: while not STOP:
start_time = time.time() start_time = time.time()
# get image
if camera_config['head_camera']['enable_zmq']:
if args.record or xr_need_local_img:
head_img, head_img_fps = img_client.get_head_frame()
if xr_need_local_img:
tv_wrapper.render_to_xr(head_img)
if camera_config['left_wrist_camera']['enable_zmq']:
if args.record:
left_wrist_img, _ = img_client.get_left_wrist_frame()
if camera_config['right_wrist_camera']['enable_zmq']:
if args.record:
right_wrist_img, _ = img_client.get_right_wrist_frame()
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)
# opencv GUI communication
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
START = False
STOP = True
if args.sim:
publish_reset_category(2, reset_pose_publisher)
elif key == ord('s'):
RECORD_TOGGLE = True
elif key == ord('a'):
if args.sim:
publish_reset_category(2, reset_pose_publisher)
# record mode
if args.record and RECORD_TOGGLE: if args.record and RECORD_TOGGLE:
RECORD_TOGGLE = False RECORD_TOGGLE = False
if not RECORD_RUNNING: if not RECORD_RUNNING:
@ -291,39 +268,40 @@ if __name__ == '__main__':
recorder.save_episode() recorder.save_episode()
if args.sim: if args.sim:
publish_reset_category(1, reset_pose_publisher) publish_reset_category(1, reset_pose_publisher)
# get input data
tele_data = tv_wrapper.get_motion_state_data()
if (args.ee == "dex3" or args.ee == "inspire1" or args.ee == "brainco") and args.xr_mode == "hand":
# get xr's tele data
tele_data = tv_wrapper.get_tele_data()
if (args.ee == "dex3" or args.ee == "inspire1" or args.ee == "brainco") and args.input_mode == "hand":
with left_hand_pos_array.get_lock(): with left_hand_pos_array.get_lock():
left_hand_pos_array[:] = tele_data.left_hand_pos.flatten() left_hand_pos_array[:] = tele_data.left_hand_pos.flatten()
with right_hand_pos_array.get_lock(): with right_hand_pos_array.get_lock():
right_hand_pos_array[:] = tele_data.right_hand_pos.flatten() right_hand_pos_array[:] = tele_data.right_hand_pos.flatten()
elif args.ee == "dex1" and args.xr_mode == "controller":
elif args.ee == "dex1" and args.input_mode == "controller":
with left_gripper_value.get_lock(): with left_gripper_value.get_lock():
left_gripper_value.value = tele_data.left_trigger_value
left_gripper_value.value = tele_data.left_ctrl_triggerValue
with right_gripper_value.get_lock(): with right_gripper_value.get_lock():
right_gripper_value.value = tele_data.right_trigger_value
elif args.ee == "dex1" and args.xr_mode == "hand":
right_gripper_value.value = tele_data.right_ctrl_triggerValue
elif args.ee == "dex1" and args.input_mode == "hand":
with left_gripper_value.get_lock(): with left_gripper_value.get_lock():
left_gripper_value.value = tele_data.left_pinch_value
left_gripper_value.value = tele_data.left_hand_pinchValue
with right_gripper_value.get_lock(): with right_gripper_value.get_lock():
right_gripper_value.value = tele_data.right_pinch_value
right_gripper_value.value = tele_data.right_hand_pinchValue
else: else:
pass pass
# high level control # high level control
if args.xr_mode == "controller" and args.motion:
if args.input_mode == "controller" and args.motion:
# quit teleoperate # quit teleoperate
if tele_data.tele_state.right_aButton:
if tele_data.right_ctrl_aButton:
START = False START = False
STOP = True STOP = True
# command robot to enter damping mode. soft emergency stop function # command robot to enter damping mode. soft emergency stop function
if tele_data.tele_state.left_thumbstick_state and tele_data.tele_state.right_thumbstick_state:
sport_client.Damp()
# control, limit velocity to within 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)
if tele_data.left_ctrl_thumbstick and tele_data.right_ctrl_thumbstick:
loco_wrapper.Damp()
# https://github.com/unitreerobotics/xr_teleoperate/issues/135, control, limit velocity to within 0.3
loco_wrapper.Move(-tele_data.left_ctrl_thumbstickValue[1] * 0.3,
-tele_data.left_ctrl_thumbstickValue[0] * 0.3,
-tele_data.right_ctrl_thumbstickValue[0]* 0.3)
# get current robot state data. # get current robot state data.
current_lr_arm_q = arm_ctrl.get_current_dual_arm_q() current_lr_arm_q = arm_ctrl.get_current_dual_arm_q()
@ -331,16 +309,16 @@ if __name__ == '__main__':
# solve ik using motor data and wrist pose, then use ik results to control arms. # solve ik using motor data and wrist pose, then use ik results to control arms.
time_ik_start = time.time() 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)
sol_q, sol_tauff = arm_ik.solve_ik(tele_data.left_wrist_pose, tele_data.right_wrist_pose, current_lr_arm_q, current_lr_arm_dq)
time_ik_end = time.time() time_ik_end = time.time()
logger_mp.debug(f"ik:\t{round(time_ik_end - time_ik_start, 6)}") logger_mp.debug(f"ik:\t{round(time_ik_end - time_ik_start, 6)}")
arm_ctrl.ctrl_dual_arm(sol_q, sol_tauff) arm_ctrl.ctrl_dual_arm(sol_q, sol_tauff)
# record data # record data
if args.record: if args.record:
RECORD_READY = recorder.is_ready()
READY = recorder.is_ready() # now ready to (2) enter RECORD_RUNNING state
# dex hand or gripper # dex hand or gripper
if args.ee == "dex3" and args.xr_mode == "hand":
if args.ee == "dex3" and args.input_mode == "hand":
with dual_hand_data_lock: with dual_hand_data_lock:
left_ee_state = dual_hand_state_array[:7] left_ee_state = dual_hand_state_array[:7]
right_ee_state = dual_hand_state_array[-7:] right_ee_state = dual_hand_state_array[-7:]
@ -348,7 +326,7 @@ if __name__ == '__main__':
right_hand_action = dual_hand_action_array[-7:] right_hand_action = dual_hand_action_array[-7:]
current_body_state = [] current_body_state = []
current_body_action = [] current_body_action = []
elif args.ee == "dex1" and args.xr_mode == "hand":
elif args.ee == "dex1" and args.input_mode == "hand":
with dual_gripper_data_lock: with dual_gripper_data_lock:
left_ee_state = [dual_gripper_state_array[0]] left_ee_state = [dual_gripper_state_array[0]]
right_ee_state = [dual_gripper_state_array[1]] right_ee_state = [dual_gripper_state_array[1]]
@ -356,17 +334,17 @@ if __name__ == '__main__':
right_hand_action = [dual_gripper_action_array[1]] right_hand_action = [dual_gripper_action_array[1]]
current_body_state = [] current_body_state = []
current_body_action = [] current_body_action = []
elif args.ee == "dex1" and args.xr_mode == "controller":
elif args.ee == "dex1" and args.input_mode == "controller":
with dual_gripper_data_lock: with dual_gripper_data_lock:
left_ee_state = [dual_gripper_state_array[0]] left_ee_state = [dual_gripper_state_array[0]]
right_ee_state = [dual_gripper_state_array[1]] right_ee_state = [dual_gripper_state_array[1]]
left_hand_action = [dual_gripper_action_array[0]] left_hand_action = [dual_gripper_action_array[0]]
right_hand_action = [dual_gripper_action_array[1]] right_hand_action = [dual_gripper_action_array[1]]
current_body_state = arm_ctrl.get_current_motor_q().tolist() 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" or args.ee == "brainco") and args.xr_mode == "hand":
current_body_action = [-tele_data.left_ctrl_thumbstickValue[1] * 0.3,
-tele_data.left_ctrl_thumbstickValue[0] * 0.3,
-tele_data.right_ctrl_thumbstickValue[0] * 0.3]
elif (args.ee == "inspire_dfx" or args.ee == "inspire_ftp" or args.ee == "brainco") and args.input_mode == "hand":
with dual_hand_data_lock: with dual_hand_data_lock:
left_ee_state = dual_hand_state_array[:6] left_ee_state = dual_hand_state_array[:6]
right_ee_state = dual_hand_state_array[-6:] right_ee_state = dual_hand_state_array[-6:]
@ -381,11 +359,7 @@ if __name__ == '__main__':
right_hand_action = [] right_hand_action = []
current_body_state = [] current_body_state = []
current_body_action = [] current_body_action = []
# head image
current_tv_image = tv_img_array.copy()
# wrist image
if WRIST:
current_wrist_image = wrist_img_array.copy()
# arm state and action # arm state and action
left_arm_state = current_lr_arm_q[:7] left_arm_state = current_lr_arm_q[:7]
right_arm_state = current_lr_arm_q[-7:] right_arm_state = current_lr_arm_q[-7:]
@ -394,17 +368,37 @@ if __name__ == '__main__':
if RECORD_RUNNING: if RECORD_RUNNING:
colors = {} colors = {}
depths = {} 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:]
if camera_config['head_camera']['binocular']:
if head_img is not None:
colors[f"color_{0}"] = head_img[:, :camera_config['head_camera']['image_shape'][1]//2]
colors[f"color_{1}"] = head_img[:, camera_config['head_camera']['image_shape'][1]//2:]
else:
logger_mp.warning("Head image is None!")
if camera_config['left_wrist_camera']['enable_zmq']:
if left_wrist_img is not None:
colors[f"color_{2}"] = left_wrist_img
else:
logger_mp.warning("Left wrist image is None!")
if camera_config['right_wrist_camera']['enable_zmq']:
if right_wrist_img is not None:
colors[f"color_{3}"] = right_wrist_img
else:
logger_mp.warning("Right wrist image is None!")
else:
if head_img is not None:
colors[f"color_{0}"] = head_img
else: else:
colors[f"color_{0}"] = current_tv_image
if 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:]
logger_mp.warning("Head image is None!")
if camera_config['left_wrist_camera']['enable_zmq']:
if left_wrist_img is not None:
colors[f"color_{1}"] = left_wrist_img
else:
logger_mp.warning("Left wrist image is None!")
if camera_config['right_wrist_camera']['enable_zmq']:
if right_wrist_img is not None:
colors[f"color_{2}"] = right_wrist_img
else:
logger_mp.warning("Right wrist image is None!")
states = { states = {
"left_arm": { "left_arm": {
"qpos": left_arm_state.tolist(), # numpy.array -> list "qpos": left_arm_state.tolist(), # numpy.array -> list
@ -470,23 +464,47 @@ if __name__ == '__main__':
except KeyboardInterrupt: except KeyboardInterrupt:
logger_mp.info("KeyboardInterrupt, exiting program...") logger_mp.info("KeyboardInterrupt, exiting program...")
finally: finally:
try:
arm_ctrl.ctrl_dual_arm_go_home() arm_ctrl.ctrl_dual_arm_go_home()
except Exception as e:
logger_mp.error(f"Failed to ctrl_dual_arm_go_home: {e}")
try:
if args.ipc: if args.ipc:
ipc_server.stop() ipc_server.stop()
else: else:
stop_listening() stop_listening()
listen_keyboard_thread.join() listen_keyboard_thread.join()
except Exception as e:
logger_mp.error(f"Failed to stop keyboard listener or ipc server: {e}")
try:
img_client.close()
except Exception as e:
logger_mp.error(f"Failed to close image client: {e}")
try:
tv_wrapper.close()
except Exception as e:
logger_mp.error(f"Failed to close televuer wrapper: {e}")
try:
if not args.motion:
status, result = motion_switcher.Exit_Debug_Mode()
logger_mp.info(f"Exit debug mode: {'Success' if status == 3104 else 'Failed'}")
except Exception as e:
logger_mp.error(f"Failed to exit debug mode: {e}")
try:
if args.sim: if args.sim:
sim_state_subscriber.stop_subscribe() sim_state_subscriber.stop_subscribe()
tv_img_shm.close()
tv_img_shm.unlink()
if WRIST:
wrist_img_shm.close()
wrist_img_shm.unlink()
except Exception as e:
logger_mp.error(f"Failed to stop sim state subscriber: {e}")
try:
if args.record: if args.record:
recorder.close() recorder.close()
except Exception as e:
logger_mp.error(f"Failed to close recorder: {e}")
logger_mp.info("Finally, exiting program.") logger_mp.info("Finally, exiting program.")
exit(0) exit(0)

2
teleop/televuer

@ -1 +1 @@
Subproject commit 86367f8c9ba248f4065b959bfc53e5f36339bf6d
Subproject commit 948f65f6852410610483345e69715a0c673a99eb

10
teleop/utils/episode_writer.py

@ -11,7 +11,7 @@ import logging_mp
logger_mp = logging_mp.get_logger(__name__) logger_mp = logging_mp.get_logger(__name__)
class EpisodeWriter(): class EpisodeWriter():
def __init__(self, task_dir, task_goal=None, frequency=30, image_size=[640, 480], rerun_log = True):
def __init__(self, task_dir, task_goal=None, task_desc = None, task_steps = None, frequency=30, image_size=[640, 480], rerun_log = True):
""" """
image_size: [width, height] image_size: [width, height]
""" """
@ -24,6 +24,10 @@ class EpisodeWriter():
} }
if task_goal is not None: if task_goal is not None:
self.text['goal'] = task_goal self.text['goal'] = task_goal
if task_desc is not None:
self.text['desc'] = task_desc
if task_steps is not None:
self.text['steps'] = task_steps
self.frequency = frequency self.frequency = frequency
self.image_size = image_size self.image_size = image_size
@ -37,7 +41,7 @@ class EpisodeWriter():
self.item_id = -1 self.item_id = -1
self.episode_id = -1 self.episode_id = -1
if os.path.exists(self.task_dir): if os.path.exists(self.task_dir):
episode_dirs = [episode_dir for episode_dir in os.listdir(self.task_dir) if 'episode_' in episode_dir]
episode_dirs = [episode_dir for episode_dir in os.listdir(self.task_dir) if 'episode_' in episode_dir and not episode_dir.endswith('.zip')]
episode_last = sorted(episode_dirs)[-1] if len(episode_dirs) > 0 else None episode_last = sorted(episode_dirs)[-1] if len(episode_dirs) > 0 else None
self.episode_id = 0 if episode_last is None else int(episode_last.split('_')[-1]) self.episode_id = 0 if episode_last is None else int(episode_last.split('_')[-1])
logger_mp.info(f"==> task_dir directory already exist, now self.episode_id is:{self.episode_id}\n") logger_mp.info(f"==> task_dir directory already exist, now self.episode_id is:{self.episode_id}\n")
@ -68,7 +72,7 @@ class EpisodeWriter():
"depth": {"width":self.image_size[0], "height":self.image_size[1], "fps":self.frequency}, "depth": {"width":self.image_size[0], "height":self.image_size[1], "fps":self.frequency},
"audio": {"sample_rate": 16000, "channels": 1, "format":"PCM", "bits":16}, # PCM_S16 "audio": {"sample_rate": 16000, "channels": 1, "format":"PCM", "bits":16}, # PCM_S16
"joint_names":{ "joint_names":{
"left_arm": ['kLeftShoulderPitch' ,'kLeftShoulderRoll', 'kLeftShoulderYaw', 'kLeftElbow', 'kLeftWristRoll', 'kLeftWristPitch', 'kLeftWristyaw'],
"left_arm": [],
"left_ee": [], "left_ee": [],
"right_arm": [], "right_arm": [],
"right_ee": [], "right_ee": [],

45
teleop/utils/ipc.py

@ -22,12 +22,7 @@ logger_mp = logging_mp.get_logger(__name__, level=logging_mp.INFO)
3) start or stop (record toggle) 3) start or stop (record toggle)
{ {
"reqid": unique id, "reqid": unique id,
"cmd": "CMD_RECORD_TOGGLE",
"info": { # optional
"task_name": "T001",
"task_desc": "pick and place apple to basket",
"item_id": 1
}
"cmd": "CMD_RECORD_TOGGLE"
} }
# Server → Client (Reply) # Server → Client (Reply)
@ -44,7 +39,6 @@ logger_mp = logging_mp.get_logger(__name__, level=logging_mp.INFO)
"msg": "reqid not provided" "msg": "reqid not provided"
| "cmd not provided" | "cmd not provided"
| "cmd not supported: {cmd}" | "cmd not supported: {cmd}"
| "info missing keys: {missing_keys}"
| "internal error msg" | "internal error msg"
} }
@ -72,19 +66,18 @@ class IPC_Server:
"CMD_RECORD_TOGGLE": "s", # start & stop (toggle record) "CMD_RECORD_TOGGLE": "s", # start & stop (toggle record)
} }
def __init__(self, on_press=None, on_info=None, get_state=None, hb_fps=10.0):
def __init__(self, on_press=None, get_state=None, hb_fps=10.0):
""" """
Args: Args:
on_press : callback(cmd:str), called for every command on_press : callback(cmd:str), called for every command
on_info : callback(data:dict), only handle CMD_RECORD_TOGGLE's task info
hb_fps : heartbeat publish frequency
get_state : callback() -> dict, provides current heartbeat state get_state : callback() -> dict, provides current heartbeat state
hb_fps : heartbeat publish frequency
""" """
if callable(on_press): if callable(on_press):
self.on_press = on_press self.on_press = on_press
else: else:
raise ValueError("[IPC_Server] on_press callback function must be provided") raise ValueError("[IPC_Server] on_press callback function must be provided")
self.on_info = on_info
if callable(get_state): if callable(get_state):
self.get_state = get_state self.get_state = get_state
else: else:
@ -172,23 +165,6 @@ class IPC_Server:
if cmd not in self.cmd_map: if cmd not in self.cmd_map:
return {"repid": reqid, "status": "error", "msg": f"cmd not supported: {cmd}"} return {"repid": reqid, "status": "error", "msg": f"cmd not supported: {cmd}"}
# CMD_RECORD_TOGGLE: optional info
if cmd == "CMD_RECORD_TOGGLE":
info = msg.get("info", None)
if info:
required_keys = ["task_name", "task_desc", "item_id"]
missing_keys = [key for key in required_keys if key not in info]
if missing_keys:
return {"repid": reqid, "status": "error", "msg": f"info missing keys: {missing_keys}"}
else:
if self.on_info:
self.on_info(info)
logger_mp.debug(f"[IPC_Server] on_info called with info: {info}")
else:
logger_mp.warning("[IPC_Server] No on_info provided")
else:
logger_mp.warning("[IPC_Server] No info provided with cmd: CMD_RECORD_TOGGLE")
# supported cmd path # supported cmd path
self.on_press(self.cmd_map[cmd]) self.on_press(self.cmd_map[cmd])
return {"repid": reqid, "status": "ok", "msg": "ok"} return {"repid": reqid, "status": "ok", "msg": "ok"}
@ -232,7 +208,7 @@ class IPC_Server:
class IPC_Client: class IPC_Client:
""" """
Inter - Process Communication Client: Inter - Process Communication Client:
- Send command/info via REQ
- Send command via REQ
- Subscribe heartbeat via SUB - Subscribe heartbeat via SUB
""" """
def __init__(self, hb_fps=10.0): def __init__(self, hb_fps=10.0):
@ -300,7 +276,7 @@ class IPC_Client:
# --------------------------- # ---------------------------
# Public API # Public API
# --------------------------- # ---------------------------
def send_data(self, cmd: str, info: dict = None) -> dict:
def send_data(self, cmd: str) -> dict:
"""Send command to server and wait reply""" """Send command to server and wait reply"""
reqid = self._make_reqid() reqid = self._make_reqid()
if not self.is_online(): if not self.is_online():
@ -308,8 +284,6 @@ class IPC_Client:
return {"repid": reqid, "status": "error", "msg": "server offline (no heartbeat)"} return {"repid": reqid, "status": "error", "msg": "server offline (no heartbeat)"}
msg = {"reqid": reqid, "cmd": cmd} msg = {"reqid": reqid, "cmd": cmd}
if cmd == "CMD_RECORD_TOGGLE" and info:
msg["info"] = info
try: try:
self.req_socket.send_json(msg) self.req_socket.send_json(msg)
# wait up to 1s for reply # wait up to 1s for reply
@ -374,13 +348,8 @@ if __name__ == "__main__":
logger_mp.info("Reply: %s", rep) logger_mp.info("Reply: %s", rep)
elif key == "s": elif key == "s":
info = {
"task_name": "T003",
"task_desc": "pick and place pear.",
"item_id": 1,
}
logger_mp.info("⏺️ Sending record toggle command...") logger_mp.info("⏺️ Sending record toggle command...")
rep = client.send_data("CMD_RECORD_TOGGLE", info=info) # optional info
rep = client.send_data("CMD_RECORD_TOGGLE")
logger_mp.info("Reply: %s", rep) logger_mp.info("Reply: %s", rep)

54
teleop/utils/motion_switcher.py

@ -0,0 +1,54 @@
# for motion switcher
from unitree_sdk2py.core.channel import ChannelFactoryInitialize
from unitree_sdk2py.comm.motion_switcher.motion_switcher_client import MotionSwitcherClient
# for loco client
from unitree_sdk2py.g1.loco.g1_loco_client import LocoClient
import time
# MotionSwitcher used to switch mode between debug mode and ai mode
class MotionSwitcher:
def __init__(self):
ChannelFactoryInitialize(0)
self.msc = MotionSwitcherClient()
self.msc.SetTimeout(1.0)
self.msc.Init()
def Enter_Debug_Mode(self):
try:
status, result = self.msc.CheckMode()
while result['name']:
self.msc.ReleaseMode()
status, result = self.msc.CheckMode()
time.sleep(1)
return status, result
except Exception as e:
return None, None
def Exit_Debug_Mode(self):
try:
status, result = self.msc.SelectMode(nameOrAlias='ai')
return status, result
except Exception as e:
return None, None
class LocoClientWrapper:
def __init__(self):
self.client = LocoClient()
self.client.SetTimeout(0.0001)
self.client.Init()
def Enter_Damp_Mode(self):
self.client.Damp()
def Move(self, vx, vy, vyaw):
self.client.Move(vx, vy, vyaw, continous_move=False)
if __name__ == '__main__':
ChannelFactoryInitialize(0)
ms = MotionSwitcher()
status, result = ms.Enter_Debug_Mode()
print("Enter debug mode:", status, result)
time.sleep(5)
status, result = ms.Exit_Debug_Mode()
print("Exit debug mode:", status, result)
time.sleep(2)
Loading…
Cancel
Save