Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions KEYBOARD_CONTROL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# 键盘控制使用说明

## 概述

已为 unitree_mujoco 添加键盘控制功能,无需手柄即可控制机器人。

## 启用键盘控制

编辑 `simulate/config.yaml` 文件:

```yaml
use_joystick: 1 # 启用控制器
joystick_type: "keyboard" # 使用键盘模式(原来是 "xbox" 或 "switch")
```

## 键盘映射

### 基础按键对照表

| 键盘按键 | 手柄按键 | 说明 |
|---------|---------|------|
| **左Ctrl** | LB (L1) | 左肩键 |
| **左Shift** | LT (L2) | 左触发器 |
| **右Ctrl** | RB (R1) | 右肩键 |
| **右Shift** | RT (R2) | 右触发器 |
| **A** | A | A 按钮 |
| **B** | B | B 按钮 |
| **X** | X | X 按钮 |
| **Y** | Y | Y 按钮 |
| **1** | F1 | 功能键1 |
| **2** | F2 | 功能键2 |
| **Tab** | Start | 开始键 |
| **Esc** | Select | 选择键 |
| **↑/←/↓/→** | D-pad ↑/←/↓/→ | 方向键 |

### 摇杆控制

| 键盘按键 | 摇杆 | 说明 |
|---------|------|------|
| **I** | 左摇杆 ly +1.0 | 前进 |
| **K** | 左摇杆 ly -1.0 | 后退 |
| **J** | 左摇杆 lx -1.0 | 左移 |
| **L** | 左摇杆 lx +1.0 | 右移 |
| **U** | 右摇杆 rx -1.0 | 左转 |
| **O** | 右摇杆 rx +1.0 | 右转 |

### 组合键示例(对应 IOSDK 遥控器操作)

键盘支持同时按多个键,模拟遥控器组合键操作:

| 键盘组合 | 手柄组合 | UserCommand | 功能 |
|---------|---------|-------------|------|
| **左Ctrl + A** | L1 + A | L1_A | - |
| **左Ctrl + B** | L1 + B | L1_B | charleston_dance |
| **左Ctrl + X** | L1 + X | L1_X | - |
| **左Ctrl + Y** | L1 + Y | L1_Y | - |
| **左Shift + A** | L2 + A | L2_A | - |
| **左Shift + B** | L2 + B | L2_B | CMCC_GPC_dance1 |
| **左Shift + X** | L2 + X | L2_X | - |
| **左Shift + Y** | L2 + Y | L2_Y | CMCC_GPC_dance2 |
| **右Ctrl + A** | R1 + A | R1_A | dancekgswing |
| **右Ctrl + B** | R1 + B | R1_B | dancedzht |
| **右Ctrl + X** | R1 + X | R1_X | dancekaraoke |
| **右Ctrl + Y** | R1 + Y | R1_Y | danceydd |
| **右Shift + A** | R2 + A | R2_A | 行走模式 |
| **右Shift + B** | R2 + B | R2_B | dancebcgm |
| **右Shift + X** | R2 + X | R2_X | dancepower |
| **右Shift + Y** | R2 + Y | R2_Y | dancemojito |
| **左Shift + 1** | L2 + F1 | L2_F1 | 紧急停止(PASSIVE) |
| **1 + A** | F1 + A | F1_A | signal_debug |

### 弹性带控制(Elastic Band,用于吊起机器人)
需要在 `config.yaml` 中设置 `enable_elastic_band: 1`

- **9**: 启用/禁用弹性带
- **7**: 缩短弹性带(吊起机器人)
- **8**: 延长弹性带(释放机器人)

### 其他
- **Backspace**: 重置仿真

### 宏按键

| 按键 | 等效按下 | 说明 |
|------|----------|------|
| **3** | **L2 + D-pad Up** | 同时触发 L2 与 D-pad 上 |
| **4** | **R2 + A** | 同时触发 R2 与 A |

## 使用示例

1. 修改配置文件:
```bash
cd ~/Documents/Code/unitree/unitree_mujoco/simulate
vim config.yaml
```

2. 修改以下行:
```yaml
use_joystick: 1
joystick_type: "keyboard"
```

3. 编译并运行:
```bash
cd build
make -j4
./unitree_mujoco
```

4. 使用键盘控制机器人移动

## 技术实现

### 新增文件
- `simulate/src/keyboard_joystick.h`: 键盘控制类实现

### 修改文件
- `simulate/src/unitree_sdk2_bridge.h`: 添加键盘支持和 `setGLFWWindow()` 方法
- `simulate/src/main.cc`: 添加全局 bridge 指针和窗口设置逻辑

### 核心类
```cpp
class KeyboardJoystick : public unitree::common::UnitreeJoystick
```

该类继承自 `UnitreeJoystick`,通过 GLFW 键盘事件模拟手柄输入。

## 注意事项

1. 键盘控制是数字输入(0 或 1),不像手柄有模拟量
2. 对角线移动会自动归一化,保持速度一致
3. 可以同时按多个键实现组合控制
4. 键盘控制与原有手柄控制互不冲突,可通过配置文件切换

## 作者
Zhang Zhen (zhangzhen@cmhi.chinamobile.com)
13 changes: 7 additions & 6 deletions simulate/config.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
robot: "go2" # Robot name, "go2", "b2", "b2w", "h1", "go2w", "g1"
robot: "g1" # Robot name, "go2", "b2", "b2w", "h1", "go2w", "g1"
robot_scene: "scene.xml" # Robot scene, /unitree_robots/[robot]/scene.xml

domain_id: 1 # Domain id
interface: "lo" # Interface
lowcmd_topic: "" # Empty uses robot default: G1 -> rt/user_lowcmd, others -> rt/lowcmd

use_joystick: 0 # Simulate Unitree WirelessController using a gamepad
joystick_type: "xbox" # support "xbox" and "switch" gamepad layout
joystick_device: "/dev/input/js0" # Device path
joystick_bits: 16 # Some game controllers may only have 8-bit accuracy
use_joystick: 1 # Simulate Unitree WirelessController using a gamepad
joystick_type: "keyboard" # support "xbox", "switch", and "keyboard"
joystick_device: "/dev/input/js0" # Device path (not used in keyboard mode)
joystick_bits: 16 # Some game controllers may only have 8-bit accuracy (not used in keyboard mode)

print_scene_information: 1 # Print link, joint and sensors information of robot

enable_elastic_band: 0 # Virtual spring band, used for lifting h1
enable_elastic_band: 1 # Virtual spring band, used for lifting h1
113 changes: 113 additions & 0 deletions simulate/src/keyboard_joystick.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#pragma once

#include <iostream>
#include <unitree/dds_wrapper/common/unitree_joystick.hpp>
#include <GLFW/glfw3.h>
#include <memory>
#include <map>

/**
* @brief Keyboard-based joystick emulation for unitree_mujoco
* @author Zhang Zhen (zhangzhen@cmhi.chinamobile.com)
*
* Keyboard mapping:
* - IJKL: Left stick (lx, ly) - movement control
* - U/O: Right stick rx (left/right turn)
* - A/B/X/Y: A/B/X/Y buttons
* - LCtrl/LShift: L1/L2
* - RCtrl/RShift: R1/R2
* - Arrow keys: D-pad (up/left/down/right)
* - 1: F1 | 2: F2
* - Tab: Start | Esc: Select
*/
class KeyboardJoystick : public unitree::common::UnitreeJoystick
{
public:
KeyboardJoystick(GLFWwindow* window)
: unitree::common::UnitreeJoystick(), window_(window)
{
if (!window_) {
std::cout << "Error: GLFW window is null." << std::endl;
exit(1);
}
std::cout << "[KeyboardJoystick] Initialized. Use keyboard to control:" << std::endl;
std::cout << " IJKL: Move (left stick)" << std::endl;
std::cout << " U/O: Turn left/right (right stick rx)" << std::endl;
std::cout << " A/B/X/Y: A/B/X/Y buttons" << std::endl;
std::cout << " LCtrl/LShift: L1/L2 | RCtrl/RShift: R1/R2" << std::endl;
std::cout << " Arrow keys: D-pad (up/left/down/right)" << std::endl;
std::cout << " 1: F1 | 2: F2 | Tab: Start | Esc: Select" << std::endl;
std::cout << " 3: Lock Stand (L2+Up) | 4: Walk (R2+A)" << std::endl;
}

void update() override
{
const bool macro_l2_up = (glfwGetKey(window_, GLFW_KEY_3) == GLFW_PRESS);
const bool macro_r2_a = (glfwGetKey(window_, GLFW_KEY_4) == GLFW_PRESS);

// ABXY buttons (A/B/X/Y)
A((glfwGetKey(window_, GLFW_KEY_A) == GLFW_PRESS) || macro_r2_a);
B(glfwGetKey(window_, GLFW_KEY_B) == GLFW_PRESS);
X(glfwGetKey(window_, GLFW_KEY_X) == GLFW_PRESS);
Y(glfwGetKey(window_, GLFW_KEY_Y) == GLFW_PRESS);

// Shoulder buttons: L1=LCtrl, L2=LShift, R1=RCtrl, R2=RShift
LB(glfwGetKey(window_, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS); // L1
LT((glfwGetKey(window_, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) || macro_l2_up); // L2
RB(glfwGetKey(window_, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS); // R1
RT((glfwGetKey(window_, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS) || macro_r2_a); // R2

// D-pad (Arrow keys)
up((glfwGetKey(window_, GLFW_KEY_UP) == GLFW_PRESS) || macro_l2_up);
left(glfwGetKey(window_, GLFW_KEY_LEFT) == GLFW_PRESS);
down(glfwGetKey(window_, GLFW_KEY_DOWN) == GLFW_PRESS);
right(glfwGetKey(window_, GLFW_KEY_RIGHT) == GLFW_PRESS);

// Function keys: F1 = 1, F2 = 2
F1(glfwGetKey(window_, GLFW_KEY_1) == GLFW_PRESS);
F2(glfwGetKey(window_, GLFW_KEY_2) == GLFW_PRESS);

// Other
back(glfwGetKey(window_, GLFW_KEY_ESCAPE) == GLFW_PRESS);
start(glfwGetKey(window_, GLFW_KEY_TAB) == GLFW_PRESS);

// Left stick (IJKL for movement)
double lx_val = 0.0;
double ly_val = 0.0;

if (glfwGetKey(window_, GLFW_KEY_J) == GLFW_PRESS) lx_val -= 1.0;
if (glfwGetKey(window_, GLFW_KEY_L) == GLFW_PRESS) lx_val += 1.0;
if (glfwGetKey(window_, GLFW_KEY_I) == GLFW_PRESS) ly_val += 1.0;
if (glfwGetKey(window_, GLFW_KEY_K) == GLFW_PRESS) ly_val -= 1.0;

// Normalize diagonal movement
if (lx_val != 0.0 && ly_val != 0.0) {
double norm = std::sqrt(lx_val * lx_val + ly_val * ly_val);
lx_val /= norm;
ly_val /= norm;
}

lx(lx_val);
ly(ly_val);

// Right stick (Arrow keys for rotation/camera)
double rx_val = 0.0;
double ry_val = 0.0;

if (glfwGetKey(window_, GLFW_KEY_U) == GLFW_PRESS) rx_val -= 1.0;
if (glfwGetKey(window_, GLFW_KEY_O) == GLFW_PRESS) rx_val += 1.0;

// Normalize diagonal movement
if (rx_val != 0.0 && ry_val != 0.0) {
double norm = std::sqrt(rx_val * rx_val + ry_val * ry_val);
rx_val /= norm;
ry_val /= norm;
}

rx(rx_val);
ry(ry_val);
}

private:
GLFWwindow* window_;
};
19 changes: 16 additions & 3 deletions simulate/src/main.cc
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ namespace
// model and data
mjModel *m = nullptr;
mjData *d = nullptr;

// global bridge pointer for keyboard joystick setup
UnitreeSDK2BridgeBase* g_bridge = nullptr;

// control noise variables
mjtNum *ctrlnoise = nullptr;
Expand Down Expand Up @@ -599,6 +602,7 @@ void *UnitreeSdk2BridgeThread(void *arg)
} else {
interface = std::make_unique<Go2Bridge>(m, d);
}
g_bridge = interface.get(); // Save pointer for keyboard joystick setup
interface->start();

while (true)
Expand All @@ -625,9 +629,9 @@ void user_key_cb(GLFWwindow* window, int key, int scancode, int act, int mods) {
if(param::config.enable_elastic_band == 1) {
if (key==GLFW_KEY_9) {
elastic_band.enable_ = !elastic_band.enable_;
} else if (key==GLFW_KEY_7 || key==GLFW_KEY_UP) {
} else if (key==GLFW_KEY_7) {
elastic_band.length_ -= 0.1;
} else if (key==GLFW_KEY_8 || key==GLFW_KEY_DOWN) {
} else if (key==GLFW_KEY_8) {
elastic_band.length_ += 0.1;
}
}
Expand Down Expand Up @@ -666,6 +670,7 @@ int main(int argc, char **argv)

mjvOption opt;
mjv_defaultOption(&opt);
opt.flags[mjVIS_CONTACTPOINT] = 1;

mjvPerturb pert;
mjv_defaultPerturb(&pert);
Expand All @@ -687,8 +692,16 @@ int main(int argc, char **argv)

// start physics thread
std::thread physicsthreadhandle(&PhysicsThread, sim.get(), param::config.robot_scene.c_str());

// Wait for bridge initialization and set GLFW window for keyboard joystick
while (!g_bridge) {
usleep(100000); // Wait 100ms
}
GLFWwindow* window = static_cast<mj::GlfwAdapter*>(sim->platform_ui.get())->window_;
g_bridge->setGLFWWindow(window);

// start simulation UI loop (blocking call)
glfwSetKeyCallback(static_cast<mj::GlfwAdapter*>(sim->platform_ui.get())->window_,user_key_cb);
glfwSetKeyCallback(window, user_key_cb);
sim->RenderLoop();
physicsthreadhandle.join();

Expand Down
5 changes: 4 additions & 1 deletion simulate/src/param.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ inline struct SimulationConfig

int domain_id;
std::string interface;
std::string lowcmd_topic;

int use_joystick;
std::string joystick_type;
Expand All @@ -35,6 +36,7 @@ inline struct SimulationConfig
robot_scene = cfg["robot_scene"].as<std::string>();
domain_id = cfg["domain_id"].as<int>();
interface = cfg["interface"].as<std::string>();
lowcmd_topic = cfg["lowcmd_topic"] ? cfg["lowcmd_topic"].as<std::string>() : "";
use_joystick = cfg["use_joystick"].as<int>();
joystick_type = cfg["joystick_type"].as<std::string>();
joystick_device = cfg["joystick_device"].as<std::string>();
Expand Down Expand Up @@ -63,6 +65,7 @@ inline po::variables_map helper(int argc, char** argv)
("network,n", po::value<std::string>(&config.interface), "DDS network interface; -n eth0")
("robot,r", po::value<std::string>(&config.robot), "Robot type; -r go2")
("scene,s", po::value<std::filesystem::path>(&config.robot_scene), "Robot scene file; -s scene_terrain.xml")
("lowcmd_topic", po::value<std::string>(&config.lowcmd_topic), "DDS LowCmd topic override; empty uses robot default")
;

po::variables_map vm;
Expand All @@ -78,4 +81,4 @@ inline po::variables_map helper(int argc, char** argv)
return vm;
}

}
}
Loading