cpp对齐官方

This commit is contained in:
cyy_mac
2026-07-30 15:25:48 +08:00
parent dbfcb95566
commit f8b849397d
36 changed files with 2561 additions and 122 deletions

2
.gitignore vendored
View File

@@ -4,6 +4,8 @@ __pycache__/
*.so
.Python
build/
build-cpp*/
CMakeFiles/
dist/
*.egg-info/
.installed.cfg

115
CMakeLists.txt Normal file
View File

@@ -0,0 +1,115 @@
cmake_minimum_required(VERSION 3.16)
project(unitree_legged_sdk VERSION 3.8.6 LANGUAGES CXX)
if(POLICY CMP0167)
cmake_policy(SET CMP0167 NEW)
endif()
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
find_package(Threads REQUIRED)
find_package(Boost REQUIRED)
option(GO1_PRO_BUILD_EXAMPLES "Build portable official-API C++ examples" ON)
option(GO1_PRO_BUILD_TESTS "Build C++ compatibility tests" ON)
add_library(unitree_legged_sdk
src/loop.cpp
src/pro_codec.cpp
src/quadruped.cpp
src/safety.cpp
src/udp.cpp
)
add_library(unitree_legged_sdk::unitree_legged_sdk ALIAS unitree_legged_sdk)
target_compile_features(unitree_legged_sdk PUBLIC cxx_std_14)
target_include_directories(unitree_legged_sdk
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
target_compile_definitions(unitree_legged_sdk PRIVATE
GO1_PRO_SOURCE_STATE_FILE="${CMAKE_CURRENT_SOURCE_DIR}/go1_pro_sdk/_data/blowfish_state.bin"
GO1_PRO_INSTALL_STATE_FILE="${CMAKE_INSTALL_FULL_DATADIR}/go1_pro_sdk/blowfish_state.bin"
)
if(TARGET Boost::headers)
set(GO1_PRO_BOOST_TARGET Boost::headers)
else()
set(GO1_PRO_BOOST_TARGET Boost::boost)
endif()
target_link_libraries(unitree_legged_sdk PUBLIC Threads::Threads ${GO1_PRO_BOOST_TARGET})
if(GO1_PRO_BUILD_EXAMPLES)
add_executable(example_official_compatible_position
examples/cpp/example_official_compatible_position.cpp)
target_link_libraries(example_official_compatible_position PRIVATE unitree_legged_sdk)
endif()
if(GO1_PRO_BUILD_TESTS)
enable_testing()
add_executable(test_cpp_compat tests/cpp/test_compat.cpp)
target_include_directories(test_cpp_compat PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
set(GO1_PRO_CAPTURE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/data/captures" CACHE PATH
"Optional private real-robot capture fixture directory")
if(EXISTS "${GO1_PRO_CAPTURE_DIR}/mcu_response_new.bin"
AND EXISTS "${GO1_PRO_CAPTURE_DIR}/mcu_response.bin"
AND EXISTS "${GO1_PRO_CAPTURE_DIR}/real_lowcmd.bin"
AND EXISTS "${GO1_PRO_CAPTURE_DIR}/real_lowcmd_decrypted.bin")
target_compile_definitions(test_cpp_compat PRIVATE
GO1_PRO_HAVE_CAPTURE_FIXTURES=1
GO1_PRO_TEST_CAPTURE="${GO1_PRO_CAPTURE_DIR}/mcu_response_new.bin"
GO1_PRO_TEST_CAPTURE_OLD="${GO1_PRO_CAPTURE_DIR}/mcu_response.bin"
GO1_PRO_TEST_LOWCMD="${GO1_PRO_CAPTURE_DIR}/real_lowcmd.bin"
GO1_PRO_TEST_LOWCMD_PLAIN="${GO1_PRO_CAPTURE_DIR}/real_lowcmd_decrypted.bin")
else()
message(STATUS "Private data/captures fixtures not found; C++ capture checks disabled")
endif()
target_link_libraries(test_cpp_compat PRIVATE unitree_legged_sdk)
add_test(NAME cpp_official_compat COMMAND test_cpp_compat)
set(GO1_PRO_TEST_INSTALL_DIR "${CMAKE_CURRENT_BINARY_DIR}/test-install")
set(GO1_PRO_TEST_CONSUMER_BUILD "${CMAKE_CURRENT_BINARY_DIR}/package-consumer")
add_test(NAME cpp_package_install
COMMAND ${CMAKE_COMMAND} --install ${CMAKE_CURRENT_BINARY_DIR}
--prefix ${GO1_PRO_TEST_INSTALL_DIR})
add_test(NAME cpp_package_configure
COMMAND ${CMAKE_COMMAND}
-S ${CMAKE_CURRENT_SOURCE_DIR}/tests/cpp/package_consumer
-B ${GO1_PRO_TEST_CONSUMER_BUILD}
-DCMAKE_PREFIX_PATH=${GO1_PRO_TEST_INSTALL_DIR}
"-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}"
"-DCMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS}")
set_tests_properties(cpp_package_configure PROPERTIES DEPENDS cpp_package_install)
add_test(NAME cpp_package_build
COMMAND ${CMAKE_COMMAND} --build ${GO1_PRO_TEST_CONSUMER_BUILD})
set_tests_properties(cpp_package_build PROPERTIES DEPENDS cpp_package_configure)
add_test(NAME cpp_package_run
COMMAND ${GO1_PRO_TEST_CONSUMER_BUILD}/package_consumer)
set_tests_properties(cpp_package_run PROPERTIES DEPENDS cpp_package_build)
endif()
install(TARGETS unitree_legged_sdk EXPORT unitree_legged_sdkTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(DIRECTORY include/unitree_legged_sdk DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES go1_pro_sdk/_data/blowfish_state.bin DESTINATION ${CMAKE_INSTALL_DATADIR}/go1_pro_sdk)
install(EXPORT unitree_legged_sdkTargets
FILE unitree_legged_sdkTargets.cmake
NAMESPACE unitree_legged_sdk::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/unitree_legged_sdk)
configure_package_config_file(
cmake/unitree_legged_sdkConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/unitree_legged_sdkConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/unitree_legged_sdk)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/unitree_legged_sdkConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/unitree_legged_sdkConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/unitree_legged_sdkConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/unitree_legged_sdk)

View File

@@ -1,6 +1,6 @@
# Go1 PRO SDK
Unitree Go1 **PRO** 机型的低层电机控制 Python SDK。完整逆向 PRO 版的私有协议Blowfish 加密 + 私有 LowCmd 格式),不依赖官方 C++ SDKMac 直连可达 480Hz 控制频率。
Unitree Go1 **PRO** 机型的低层电机控制 Python/C++ SDK。完整逆向 PRO 版的私有协议Blowfish 加密 + 私有 LowCmd 格式),不依赖官方二进制库Mac 直连可达 480Hz 控制频率。
> 这是为 PRO 准备的 SDK。如果你有 **EDU** 版机器狗,请用原版 [free-dog-sdk](https://github.com/Bin4ry/free-dog-sdk) — PRO 跟 EDU 协议有显著差异,互不兼容。
@@ -24,7 +24,9 @@ Unitree Go1 **PRO** 机型的低层电机控制 Python SDK。完整逆向 PRO
```python
import robot_interface as sdk
udp = sdk.UDP(sdk.LOWLEVEL, 8080, "192.168.123.10", 8007)
LOWLEVEL = 0xff # 官方 wrapper 不导出这个常量,官方示例也是在应用中定义
udp = sdk.UDP(LOWLEVEL, 8080, "192.168.123.10", 8007)
safe = sdk.Safety(sdk.LeggedType.Go1)
cmd, state = sdk.LowCmd(), sdk.LowState()
udp.InitCmdData(cmd)
@@ -37,7 +39,33 @@ udp.Send()
```
兼容范围和协议差异见 [`docs/OFFICIAL_API_COMPATIBILITY.md`](docs/OFFICIAL_API_COMPATIBILITY.md)。
官方 `HighCmd/HighState` UDP 通道和 C++ ABI 当前不在兼容范围内。
官方 `HighCmd/HighState` UDP 通道和二进制 ABI 当前不在兼容范围内。
需要同一份代码同时运行在本项目和官方 SDK 上时,只使用官方实际导出的接口;参考
[`examples/example_official_compatible_position.py`](examples/example_official_compatible_position.py)。
### 官方 C++ SDK 兼容接口
同一份低层 C++ 源码可分别链接本项目或官方 `unitree_legged_sdk`
```cpp
#include "unitree_legged_sdk/unitree_legged_sdk.h"
using namespace UNITREE_LEGGED_SDK;
UDP udp(LOWLEVEL, 8090, "192.168.123.10", 8007);
Safety safe(LeggedType::Go1);
LowCmd cmd{};
udp.InitCmdData(cmd);
```
```bash
cmake -S . -B build-cpp -DCMAKE_BUILD_TYPE=Release
cmake --build build-cpp
ctest --test-dir build-cpp --output-on-failure
```
PRO 环境链接本项目生成的 `libunitree_legged_sdk`;官方机器人环境链接官方同名库。
参考 [`examples/cpp/example_official_compatible_position.cpp`](examples/cpp/example_official_compatible_position.cpp)。
Blowfish state 默认随库安装,也可用 `GO1_PRO_BLOWFISH_STATE` 指定。
### 高层控制 (走/跳/姿态, 通过 sportMode 系统)
@@ -151,6 +179,10 @@ go1_pro_sdk/
├── safety/ # PositionLimit / PowerProtect / PositionProtect
├── utils/ # CRC, 浮点编解码, 关节常量
└── _data/ # blowfish_state.bin
include/unitree_legged_sdk/ # 官方 C++ 头文件兼容层
src/ # PRO UDP/Blowfish/Safety 实现
tests/cpp/ # C++ 契约、抓包和安装消费测试
```
## 文档

View File

@@ -0,0 +1,7 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(Threads)
find_dependency(Boost)
include("${CMAKE_CURRENT_LIST_DIR}/unitree_legged_sdkTargets.cmake")
check_required_components(unitree_legged_sdk)

View File

@@ -14,6 +14,10 @@
导入和主要调用流程,同时底层会使用 Go1 PRO 所需的 Blowfish 加密和 616 字节
私有 `LowCmd` 协议。
反过来,要让本项目编写的程序直接在官方 SDK 上复现,应用必须导入
`robot_interface`,并限制在官方 wrapper 真正导出的公共子集内。应用不应直接导入
`go1_pro_sdk`
## 接口矩阵
| 官方接口 | 本项目原接口 | 当前兼容状态 |
@@ -29,8 +33,8 @@
| `Safety.PowerProtect` | `power_protect` | 调用兼容,算法不是官方闭源实现 |
| `Safety.PositionProtect` | `position_protect` | 调用兼容,保护动作更保守 |
| `HighCmd/HighState` | MQTT `Go1/Go1MQTT` | 类型已提供UDP 传输不支持 |
| `Loop/LoopFunc` | 无 | 官方 Python wrapper 本身未导出 |
| C++ headers/static library | 无 | 不兼容 C++ 源码 ABI |
| `Loop/LoopFunc` | 无 | C++ 已提供;官方 Python wrapper 本身未导出 |
| C++ headers/static library | 无 | 低层 C++ 源码兼容;不承诺二进制 ABI |
## 关键协议差异
@@ -49,7 +53,10 @@ EDU SDK 相同。
```python
import robot_interface as sdk
udp = sdk.UDP(sdk.LOWLEVEL, 8080, "192.168.123.10", 8007)
LOWLEVEL = 0xff
FR_1 = 1
udp = sdk.UDP(LOWLEVEL, 8080, "192.168.123.10", 8007)
safe = sdk.Safety(sdk.LeggedType.Go1)
cmd = sdk.LowCmd()
state = sdk.LowState()
@@ -58,16 +65,84 @@ udp.InitCmdData(cmd)
udp.Recv()
udp.GetRecv(state)
cmd.motorCmd[sdk.FR_1].q = 1.2
cmd.motorCmd[sdk.FR_1].dq = 0.0
cmd.motorCmd[sdk.FR_1].Kp = 5.0
cmd.motorCmd[sdk.FR_1].Kd = 1.0
cmd.motorCmd[FR_1].q = 1.2
cmd.motorCmd[FR_1].dq = 0.0
cmd.motorCmd[FR_1].Kp = 5.0
cmd.motorCmd[FR_1].Kd = 1.0
safe.PositionLimit(cmd)
safe.PowerProtect(cmd, state, 1)
udp.SetSend(cmd)
udp.Send()
```
这里故意不使用 `sdk.LOWLEVEL``sdk.FR_1`、上下文管理器或 `close()`:这些是本项目
提供的便利能力,但不属于官方 Python wrapper 的导出契约。
## C++ 同源码用法
C++ 公共入口、命名空间、结构字段和低层方法签名与官方 v3.8.6 对齐:
```cpp
#include "unitree_legged_sdk/unitree_legged_sdk.h"
using namespace UNITREE_LEGGED_SDK;
UDP udp(LOWLEVEL, 8090, "192.168.123.10", 8007);
Safety safe(LeggedType::Go1);
LowCmd cmd{};
LowState state{};
udp.InitCmdData(cmd);
```
业务源码和 `#include` 不变只切换链接库。PRO 端用本项目 CMake 目标
`unitree_legged_sdk`,官方 Go1 端用官方仓库的 `libunitree_legged_sdk.a`。本项目已经用
官方头文件反向编译可移植示例,并用本项目头文件编译官方仓库的五个 C++ 示例。
本项目构建方法:
```bash
cmake -S . -B build-cpp -DCMAKE_BUILD_TYPE=Release
cmake --build build-cpp
ctest --test-dir build-cpp --output-on-failure
```
标准四参数 `UDP(LOWLEVEL, ...)` 会在库内部完成 PRO 编解码。默认从安装数据目录读取
Blowfish state需要覆盖时设置 `GO1_PRO_BLOWFISH_STATE=/path/to/blowfish_state.bin`
## 可移植程序规则
要求同一份源码直接运行时,主控制流程只使用:
- `import robot_interface as sdk`
- `sdk.UDP``sdk.Safety``sdk.LeggedType`
- `sdk.LowCmd/LowState``MotorCmd/MotorState``IMU/BmsState`
- `InitCmdData/Recv/GetRecv/SetSend/Send`
- 官方结构中公开的字段,例如 `motorCmd[i].q``motorState[i].q`
所有官方结构对象都应先无参构造,再逐字段赋值。例如使用
`motor = sdk.MotorCmd(); motor.q = 1.0`,不要写 `sdk.MotorCmd(q=1.0)`。后者是 Python
dataclass 常见写法,但官方 pybind wrapper 不接受构造参数。
枚举也按官方对象使用,例如 `sdk.Safety(sdk.LeggedType.Go1)`。不要把枚举当作整数比较
或直接写 `sdk.Safety(2)`;本项目的严格兼容入口会和官方一样拒绝后一种写法。
以下接口属于 PRO 扩展,使用后程序不再能在只安装官方 SDK 的环境中直接运行:
- `from go1_pro_sdk import ...`
- `MCUClient``Go1``Go1MQTT`
- `LowCmd.set_motor()``all_damping()`
- `LowState.remote``apply_safety()`
- `wake_mcu()``safe_stop()`、Blowfish 和快速 C++ builder。
推荐把程序分为“官方公共控制核心”和“可选 PRO 增强”两个模块。公共核心不能导入
增强模块;增强模块可以包装公共核心,但必须允许在官方环境中完全不加载。
`Send()``Recv()``SetSend()` 的具体正整数来自线协议和底层 socket不属于跨
SDK 稳定契约。初始化时可以用 `Recv() > 0` 判断是否收到包,但不应比较具体字节数。
首次进入 PRO 低层通道前,应先反复发送 `InitCmdData()` 产生的 stop sentinel并在
收到第一帧 `LowState` 后再写入有效位置目标。可移植示例已采用这一时序,避免在状态
仍为全零默认值时直接驱动关节。
`UDP(HIGHLEVEL, ..., "192.168.123.161", 8082)` 不会被静默映射到 MQTT。两种通道
状态模型和时序不同,兼容层会明确抛出 `NotImplementedError`。高层控制继续使用
`Go1``Go1MQTT`
@@ -85,5 +160,10 @@ udp.Send()
类型不保证完全相同。
- 本项目 `LowState` 是 PRO 抓包格式的解释,不应按官方 `#pragma pack(1)` 结构大小
直接做内存映射。
- 官方 C++ 用户需要单独的 C++ facade 和库链接方案Python 兼容模块不能让现有
C++ 程序直接重编译通过。
- C++ 类型布局和公开符号用于源码重编译兼容,不保证本项目库与官方预编译对象之间
的 ABI 互换;切换实现时应重新编译应用。
- 两个自定义包长 C++ `UDP` 构造函数和 `SetSend(char*)` 提供原始 UDP 透传,但不会
自动把任意自定义结构转换为 PRO 私有协议;主动断连时间和 accessible 时间参数
仅保留调用形状。Python 兼容入口仍只保证标准四参数 `LOWLEVEL` 通道。
- 官方 SDK 和本项目都提供顶层 `robot_interface`,同一个 Python 环境不要同时安装
两种实现。应在 PRO 环境安装本项目,在官方机器人环境安装官方 SDK。

View File

@@ -0,0 +1,60 @@
#include "unitree_legged_sdk/unitree_legged_sdk.h"
#include <chrono>
#include <cmath>
#include <iostream>
#include <thread>
using namespace UNITREE_LEGGED_SDK;
int main() {
constexpr float dt = 0.002f;
UDP udp(LOWLEVEL, 8090, UDP_SERVER_IP_BASIC, UDP_SERVER_PORT);
Safety safety(LeggedType::Go1);
LowCmd cmd{};
LowState state{};
udp.InitCmdData(cmd);
bool have_state = false;
for (int attempt = 0; attempt < 1000 && !have_state; ++attempt) {
udp.SetSend(cmd);
udp.Send();
if (udp.Recv() > 0) {
udp.GetRecv(state);
have_state = state.head[0] == 0xfe && state.head[1] == 0xef;
}
std::this_thread::sleep_for(std::chrono::duration<float>(dt));
}
if (!have_state) {
std::cerr << "No LowState received; active position command was not enabled.\n";
return 1;
}
const float initial = state.motorState[FR_1].q;
for (int step = 0; step < 2500; ++step) {
udp.Recv();
udp.GetRecv(state);
const float phase = static_cast<float>(step) * dt;
cmd.motorCmd[FR_1].q = initial + 0.15f * std::sin(phase * 2.0f);
cmd.motorCmd[FR_1].dq = 0.0f;
cmd.motorCmd[FR_1].Kp = 5.0f;
cmd.motorCmd[FR_1].Kd = 1.0f;
cmd.motorCmd[FR_1].tau = 0.0f;
safety.PositionLimit(cmd);
if (safety.PowerProtect(cmd, state, 1) < 0) {
udp.InitCmdData(cmd);
udp.SetSend(cmd);
udp.Send();
return 2;
}
safety.PositionProtect(cmd, state, 0.5);
udp.SetSend(cmd);
udp.Send();
std::this_thread::sleep_for(std::chrono::duration<float>(dt));
}
udp.InitCmdData(cmd);
udp.SetSend(cmd);
udp.Send();
return 0;
}

View File

@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Low-level example that runs with either SDK's ``robot_interface`` module."""
import time
import robot_interface as sdk
# Unitree's official Python wrapper does not export these C++ constants.
LOWLEVEL = 0xff
FR_1 = 1
def control_step(udp, safe, cmd, state, target_q=1.2):
"""Execute one control iteration using only the official Python API."""
udp.Recv()
udp.GetRecv(state)
motor = cmd.motorCmd[FR_1]
motor.q = target_q
motor.dq = 0.0
motor.Kp = 5.0
motor.Kd = 1.0
motor.tau = 0.0
safe.PositionLimit(cmd)
if safe.PowerProtect(cmd, state, 1) < 0:
raise RuntimeError("PowerProtect rejected the command")
udp.SetSend(cmd)
udp.Send()
def initialize_transport(udp, cmd, state, frames=50, dt=0.01):
"""Send only stop sentinels until the PRO MCU can return initial state."""
udp.SetSend(cmd)
received = False
for _ in range(frames):
udp.Send()
time.sleep(dt)
if udp.Recv() > 0:
received = True
udp.GetRecv(state)
if not received:
raise TimeoutError("No LowState received during initialization")
def main():
udp = sdk.UDP(LOWLEVEL, 8080, "192.168.123.10", 8007)
safe = sdk.Safety(sdk.LeggedType.Go1)
cmd = sdk.LowCmd()
state = sdk.LowState()
udp.InitCmdData(cmd)
initialize_transport(udp, cmd, state)
while True:
control_step(udp, safe, cmd, state)
time.sleep(0.002)
if __name__ == "__main__":
main()

View File

@@ -913,7 +913,7 @@ PyObject* FastLowCmdBuilder_decrypt_lowstate(FastLowCmdBuilder* self, PyObject*
q_raw[i] = static_cast<double>(get_f32_le(m + 13));
dq_raw[i] = static_cast<double>(get_f32_le(m + 17));
ddq_raw[i] = static_cast<int>(get_i16_le(m + 21));
temperature[i] = static_cast<int>(m[24]);
temperature[i] = static_cast<int>(m[23]);
reserve0[i] = static_cast<int>(get_u32_le(m + 24));
reserve1[i] = static_cast<int>(get_u32_le(m + 28));
}

View File

@@ -83,7 +83,7 @@ def build_low_cmd_plain(lowcmd: LowCmd) -> bytes:
raise ValueError(f'motorCmd 必须有 20 项, got {len(lowcmd.motorCmd)}')
offset = 22
for motor in lowcmd.motorCmd:
motor_bytes = motor.to_bytes()
motor_bytes = MotorCmd.to_bytes(motor)
assert len(motor_bytes) == 27, f'motorCmd 序列化长度错: {len(motor_bytes)}'
cmd[offset:offset + 27] = motor_bytes
offset += 27

View File

@@ -7,23 +7,37 @@ encrypted protocol implemented by this project.
from copy import deepcopy
from dataclasses import dataclass, field, fields, is_dataclass
from enum import IntEnum
from enum import Enum
import struct
from typing import List, Optional
from ..connection.mcu_client import MCUClient
from ..safety import position_limit, position_protect, power_protect
from ..safety import PowerProtectViolation
from ..types import BmsCmd, BmsState, IMU, LowCmd, LowState, MotorCmd, MotorState
from ..types import (
BmsCmd as _NativeBmsCmd,
BmsState as _NativeBmsState,
IMU as _NativeIMU,
LowCmd as _NativeLowCmd,
LowState as _NativeLowState,
MotorCmd as _NativeMotorCmd,
MotorState as _NativeMotorState,
)
from ..utils.constants import (
HIGHLEVEL, LOWLEVEL, TRIGERLEVEL, PosStopF, VelStopF,
HIGHLEVEL, LOWLEVEL, PosStopF, VelStopF,
MCU_IP, MCU_PORT,
FR_, FL_, RR_, RL_,
FR_0, FR_1, FR_2, FL_0, FL_1, FL_2,
RR_0, RR_1, RR_2, RL_0, RL_1, RL_2,
)
class LeggedType(IntEnum):
class _OfficialEnum(Enum):
def __int__(self):
return self.value
def __index__(self):
return self.value
class LeggedType(_OfficialEnum):
Aliengo = 0
A1 = 1
Go1 = 2
@@ -36,7 +50,7 @@ Go1 = LeggedType.Go1
B1 = LeggedType.B1
class RecvEnum(IntEnum):
class RecvEnum(_OfficialEnum):
nonBlock = 0x00
block = 0x01
blockTimeout = 0x02
@@ -47,21 +61,115 @@ block = RecvEnum.block
blockTimeout = RecvEnum.blockTimeout
@dataclass
class _HiddenAttribute:
def __get__(self, instance, owner=None):
raise AttributeError("not part of Unitree's official Python API")
class BmsCmd(_NativeBmsCmd):
def __init__(self):
super().__init__()
class BmsState(_NativeBmsState):
from_bytes = _HiddenAttribute()
voltage_mv = _HiddenAttribute()
voltage_v = _HiddenAttribute()
current_a = _HiddenAttribute()
def __init__(self):
super().__init__()
class IMU(_NativeIMU):
from_bytes = _HiddenAttribute()
def __init__(self):
super().__init__()
self.quaternion = [0.0] * 4
self.gyroscope = [0.0] * 3
self.accelerometer = [0.0] * 3
self.rpy = [0.0] * 3
class MotorCmd(_NativeMotorCmd):
to_bytes = _HiddenAttribute()
def __init__(self):
super().__init__()
self.mode = 0
class MotorState(_NativeMotorState):
from_bytes = _HiddenAttribute()
estimated_current = _HiddenAttribute()
def __init__(self):
super().__init__()
class LowCmd(_NativeLowCmd):
set_motor = _HiddenAttribute()
all_damping = _HiddenAttribute()
def __init__(self):
super().__init__()
self.head = [0, 0]
self.levelFlag = 0
self.frameReserve = 0
self.SN = [0, 0]
self.version = [0, 0]
self.bandWidth = 0
self.motorCmd = [MotorCmd() for _ in range(20)]
self.bms = BmsCmd()
self.wirelessRemote = [0] * 40
self.reserve = 0
self.crc = 0
class LowState(_NativeLowState):
remote = _HiddenAttribute()
def __init__(self):
super().__init__()
self.head = [0, 0]
self.SN = [0, 0]
self.version = [0, 0]
self.imu = IMU()
self.motorState = [MotorState() for _ in range(20)]
self.footForce = [0] * 4
self.footForceEst = [0] * 4
self.bms = BmsState()
self.wirelessRemote = [0] * 40
self.reserve = 0
self.crc = 0
@dataclass(init=False)
class Cartesian:
x: float = 0.0
y: float = 0.0
z: float = 0.0
def __init__(self):
self.x = 0.0
self.y = 0.0
self.z = 0.0
@dataclass
@dataclass(init=False)
class LED:
r: int = 0
g: int = 0
b: int = 0
def __init__(self):
self.r = 0
self.g = 0
self.b = 0
@dataclass
@dataclass(init=False)
class HighState:
head: List[int] = field(default_factory=lambda: [0, 0])
levelFlag: int = 0
@@ -89,8 +197,35 @@ class HighState:
reserve: int = 0
crc: int = 0
def __init__(self):
self.head = [0, 0]
self.levelFlag = 0
self.frameReserve = 0
self.SN = [0, 0]
self.version = [0, 0]
self.bandWidth = 0
self.imu = IMU()
self.motorState = [MotorState() for _ in range(20)]
self.bms = BmsState()
self.footForce = [0] * 4
self.footForceEst = [0] * 4
self.mode = 0
self.progress = 0.0
self.gaitType = 0
self.footRaiseHeight = 0.0
self.position = [0.0] * 3
self.bodyHeight = 0.0
self.velocity = [0.0] * 3
self.yawSpeed = 0.0
self.rangeObstacle = [0.0] * 4
self.footPosition2Body = [Cartesian() for _ in range(4)]
self.footSpeed2Body = [Cartesian() for _ in range(4)]
self.wirelessRemote = [0] * 40
self.reserve = 0
self.crc = 0
@dataclass
@dataclass(init=False)
class HighCmd:
head: List[int] = field(default_factory=lambda: [0, 0])
levelFlag: int = 0
@@ -113,8 +248,30 @@ class HighCmd:
reserve: int = 0
crc: int = 0
def __init__(self):
self.head = [0, 0]
self.levelFlag = 0
self.frameReserve = 0
self.SN = [0, 0]
self.version = [0, 0]
self.bandWidth = 0
self.mode = 0
self.gaitType = 0
self.speedLevel = 0
self.footRaiseHeight = 0.0
self.bodyHeight = 0.0
self.position = [0.0] * 2
self.euler = [0.0] * 3
self.velocity = [0.0] * 2
self.yawSpeed = 0.0
self.bms = BmsCmd()
self.led = [LED() for _ in range(4)]
self.wirelessRemote = [0] * 40
self.reserve = 0
self.crc = 0
@dataclass
@dataclass(init=False)
class UDPState:
TotalCount: int = 0
SendCount: int = 0
@@ -124,6 +281,15 @@ class UDPState:
RecvCRCError: int = 0
RecvLoseError: int = 0
def __init__(self):
self.TotalCount = 0
self.SendCount = 0
self.RecvCount = 0
self.SendError = 0
self.FlagError = 0
self.RecvCRCError = 0
self.RecvLoseError = 0
def _copy_object(source, target) -> None:
if is_dataclass(target):
@@ -135,6 +301,54 @@ def _copy_object(source, target) -> None:
setattr(target, name, deepcopy(getattr(source, name)))
def _u32_pair(value) -> List[int]:
if isinstance(value, (bytes, bytearray, memoryview)):
return list(struct.unpack('<2I', bytes(value)))
return [int(item) for item in value]
def _u32(value) -> int:
if isinstance(value, (bytes, bytearray, memoryview)):
return int.from_bytes(value, 'little')
return int(value)
def _copy_low_state(source, target: LowState) -> None:
if isinstance(source.head, int):
target.head = [source.head & 0xff, (source.head >> 8) & 0xff]
else:
target.head = list(source.head)
target.levelFlag = int(source.levelFlag)
target.frameReserve = int(source.frameReserve)
target.SN = _u32_pair(source.SN)
target.version = _u32_pair(source.version)
target.bandWidth = int(source.bandWidth)
target.imu = IMU()
for name in ('quaternion', 'gyroscope', 'accelerometer', 'rpy'):
setattr(target.imu, name, list(getattr(source.imu, name)))
target.imu.temperature = int(source.imu.temperature)
target.motorState = []
for source_motor in source.motorState:
motor = MotorState()
_copy_object(source_motor, motor)
motor.reserve = list(source_motor.reserve)
target.motorState.append(motor)
target.bms = BmsState()
_copy_object(source.bms, target.bms)
target.bms.BQ_NTC = list(source.bms.BQ_NTC)
target.bms.MCU_NTC = list(source.bms.MCU_NTC)
target.bms.cell_vol = list(source.bms.cell_vol)
target.footForce = list(source.footForce)
target.footForceEst = list(source.footForceEst)
target.tick = int(source.tick)
target.wirelessRemote = list(source.wirelessRemote)
target.reserve = _u32(source.reserve)
target.crc = _u32(source.crc)
class UDP:
"""Official-style UDP facade backed by :class:`MCUClient`.
@@ -144,33 +358,32 @@ class UDP:
"""
def __init__(self, *args):
if len(args) >= 4 and isinstance(args[2], str):
self.level = int(args[0])
self.localPort = int(args[1])
self.targetIP = args[2]
self.targetPort = int(args[3])
if len(args) == 4 and isinstance(args[2], str):
self._level = int(args[0])
self._local_port = int(args[1])
self._target_ip = args[2]
self._target_port = int(args[3])
self._recv_type = RecvEnum.nonBlock
self._custom_lengths = False
elif len(args) >= 5 and isinstance(args[1], str):
self.level = None
self.localPort = int(args[0])
self.targetIP = args[1]
self.targetPort = int(args[2])
self._recv_type = RecvEnum(args[6]) if len(args) > 6 else RecvEnum.nonBlock
elif len(args) == 7 and isinstance(args[1], str):
self._level = None
self._local_port = int(args[0])
self._target_ip = args[1]
self._target_port = int(args[2])
self._recv_type = RecvEnum(args[6])
self._custom_lengths = True
elif len(args) >= 3:
self.level = None
self.localPort = int(args[0])
self.targetIP = MCU_IP
self.targetPort = MCU_PORT
self._recv_type = RecvEnum(args[4]) if len(args) > 4 else RecvEnum.nonBlock
elif len(args) == 6:
self._level = None
self._local_port = int(args[0])
self._target_ip = MCU_IP
self._target_port = MCU_PORT
self._recv_type = RecvEnum(args[4])
self._custom_lengths = True
else:
raise TypeError("UDP expects one of the three official constructor signatures")
self.localIP = "0.0.0.0"
self.accessible = False
self.udpState = UDPState()
self._accessible = False
self._udp_state = UDPState()
self._client: Optional[MCUClient] = None
self._send_cmd: Optional[LowCmd] = None
self._recv_state: Optional[LowState] = None
@@ -178,39 +391,34 @@ class UDP:
self._disconnect_time = None
self._accessible_time = None
def __enter__(self):
return self
def __exit__(self, *_):
self.close()
def close(self):
if self._client is not None:
self._client.close()
def __del__(self):
client = getattr(self, '_client', None)
if client is not None:
client.close()
self._client = None
def _ensure_client(self) -> MCUClient:
if self.level == HIGHLEVEL:
if self._level == HIGHLEVEL:
raise NotImplementedError(
"HighCmd/HighState UDP (:8082) is not implemented by this "
"compatibility layer; use go1_pro_sdk.Go1 or Go1MQTT"
)
if self.level not in (LOWLEVEL, None) or self._custom_lengths:
if self._level not in (LOWLEVEL, None) or self._custom_lengths:
raise NotImplementedError("only the official LOWLEVEL constructor is supported")
if self._client is None:
self._client = MCUClient(
mcu_ip=self.targetIP,
mcu_port=self.targetPort,
local_port=self.localPort,
mcu_ip=self._target_ip,
mcu_port=self._target_port,
local_port=self._local_port,
)
self.localPort = self._client.local_port
self._local_port = self._client.local_port
return self._client
def SetIpPort(self, targetIP, targetPort):
if self._client is not None:
raise RuntimeError("SetIpPort must be called before the first Send/Recv")
self.targetIP = str(targetIP)
self.targetPort = int(targetPort)
self._target_ip = str(targetIP)
self._target_port = int(targetPort)
def SetRecvTimeout(self, time):
self._recv_timeout_ms = max(0, int(time))
@@ -223,16 +431,22 @@ class UDP:
def InitCmdData(self, cmd):
if isinstance(cmd, HighCmd):
_copy_object(HighCmd(levelFlag=HIGHLEVEL), cmd)
template = HighCmd()
template.head = [0xfe, 0xef]
template.levelFlag = HIGHLEVEL
_copy_object(template, cmd)
return None
if not isinstance(cmd, LowCmd):
raise TypeError("InitCmdData expects LowCmd or HighCmd")
template = LowCmd(levelFlag=LOWLEVEL)
template.motorCmd = [
MotorCmd(mode=0x0A, q=PosStopF, dq=VelStopF)
for _ in range(20)
]
template = LowCmd()
template.head = [0xfe, 0xef]
template.levelFlag = LOWLEVEL
template.bandWidth = 0x3ac0
for motor in template.motorCmd:
motor.mode = 0x0A
motor.q = PosStopF
motor.dq = VelStopF
_copy_object(template, cmd)
return None
@@ -250,18 +464,18 @@ class UDP:
raise RuntimeError("call InitCmdData and SetSend before Send")
if isinstance(self._send_cmd, HighCmd):
self._ensure_client()
self.udpState.TotalCount += 1
self._udp_state.TotalCount += 1
try:
sent = self._ensure_client().send(self._send_cmd)
except Exception:
self.udpState.SendError += 1
self._udp_state.SendError += 1
raise
self.udpState.SendCount += 1
self._udp_state.SendCount += 1
return sent
def Recv(self):
client = self._ensure_client()
self.udpState.TotalCount += 1
self._udp_state.TotalCount += 1
if self._recv_type == RecvEnum.nonBlock:
state = client.recv_latest()
else:
@@ -270,8 +484,8 @@ class UDP:
if state is None:
return 0
self._recv_state = state
self.accessible = True
self.udpState.RecvCount += 1
self._accessible = True
self._udp_state.RecvCount += 1
return 858
def GetRecv(self, state):
@@ -280,7 +494,7 @@ class UDP:
if not isinstance(state, LowState):
raise TypeError("GetRecv expects LowState or HighState")
if self._recv_state is not None:
_copy_object(self._recv_state, state)
_copy_low_state(self._recv_state, state)
return None
@@ -288,8 +502,10 @@ class Safety:
"""Official method names with conservative local safety behavior."""
def __init__(self, legged_type):
self.legged_type = LeggedType(legged_type)
if self.legged_type != LeggedType.Go1:
if not isinstance(legged_type, LeggedType):
raise TypeError("Safety expects a LeggedType value")
self._legged_type = legged_type
if self._legged_type != LeggedType.Go1:
raise ValueError("go1_pro_sdk only supports LeggedType.Go1")
def PositionLimit(self, lowcmd):
@@ -303,26 +519,13 @@ class Safety:
return -1
return 0
def PositionProtect(self, lowcmd, lowstate, limit=0.087):
def PositionProtect(self, lowcmd, lowstate, limit):
return -1 if position_protect(lowcmd, lowstate, limit) else 0
def VersionSDK():
return "go1_pro_sdk robot_interface compatibility (official API v3.8.6)"
def InitEnvironment():
return 0
__all__ = [
"HIGHLEVEL", "LOWLEVEL", "TRIGERLEVEL", "PosStopF", "VelStopF",
"FR_", "FL_", "RR_", "RL_",
"FR_0", "FR_1", "FR_2", "FL_0", "FL_1", "FL_2",
"RR_0", "RR_1", "RR_2", "RL_0", "RL_1", "RL_2",
"LeggedType", "Aliengo", "A1", "Go1", "B1",
"RecvEnum", "nonBlock", "block", "blockTimeout",
"UDP", "Safety", "BmsCmd", "BmsState", "Cartesian", "IMU", "LED",
"MotorState", "MotorCmd", "LowState", "LowCmd", "HighState", "HighCmd",
"UDPState", "VersionSDK", "InitEnvironment",
"UDPState",
]

View File

@@ -71,7 +71,8 @@ class MotorState:
ms.q_raw = hex_to_float(data[13:17])
ms.dq_raw = hex_to_float(data[17:21])
ms.ddq_raw = float(int.from_bytes(data[21:23], 'little', signed=True))
ms.temperature = data[24]
# PRO wire layout stores the signed temperature before the two reserve words.
ms.temperature = data[23]
ms.reserve = [
int.from_bytes(data[24:28], 'little'),
int.from_bytes(data[28:32], 'little'),

View File

@@ -0,0 +1,11 @@
#ifndef GO1_PRO_UNITREE_LEGGED_A1_CONST_H_
#define GO1_PRO_UNITREE_LEGGED_A1_CONST_H_
namespace UNITREE_LEGGED_SDK {
constexpr double a1_Hip_max = 0.802;
constexpr double a1_Hip_min = -0.802;
constexpr double a1_Thigh_max = 4.19;
constexpr double a1_Thigh_min = -1.05;
constexpr double a1_Calf_max = -0.916;
constexpr double a1_Calf_min = -2.7;
} // namespace UNITREE_LEGGED_SDK
#endif

View File

@@ -0,0 +1,11 @@
#ifndef GO1_PRO_UNITREE_LEGGED_ALIENGO_CONST_H_
#define GO1_PRO_UNITREE_LEGGED_ALIENGO_CONST_H_
namespace UNITREE_LEGGED_SDK {
constexpr double aliengo_Hip_max = 1.047;
constexpr double aliengo_Hip_min = -0.873;
constexpr double aliengo_Thigh_max = 3.927;
constexpr double aliengo_Thigh_min = -0.524;
constexpr double aliengo_Calf_max = -0.611;
constexpr double aliengo_Calf_min = -2.775;
} // namespace UNITREE_LEGGED_SDK
#endif

View File

@@ -0,0 +1,11 @@
#ifndef GO1_PRO_UNITREE_LEGGED_B1_CONST_H_
#define GO1_PRO_UNITREE_LEGGED_B1_CONST_H_
namespace UNITREE_LEGGED_SDK {
constexpr double b1_Hip_max = 0.75;
constexpr double b1_Hip_min = -0.75;
constexpr double b1_Thigh_max = 3.5;
constexpr double b1_Thigh_min = -1.0;
constexpr double b1_Calf_max = -0.6;
constexpr double b1_Calf_min = -2.6;
} // namespace UNITREE_LEGGED_SDK
#endif

View File

@@ -0,0 +1,179 @@
#ifndef GO1_PRO_UNITREE_LEGGED_COMM_H_
#define GO1_PRO_UNITREE_LEGGED_COMM_H_
#include <array>
#include <cstdint>
namespace UNITREE_LEGGED_SDK {
constexpr int HIGHLEVEL = 0xee;
constexpr int LOWLEVEL = 0xff;
constexpr int TRIGERLEVEL = 0xf0;
constexpr double PosStopF = 2.146E+9f;
constexpr double VelStopF = 16000.0f;
extern const int HIGH_CMD_LENGTH;
extern const int HIGH_STATE_LENGTH;
extern const int LOW_CMD_LENGTH;
extern const int LOW_STATE_LENGTH;
#pragma pack(push, 1)
struct BmsCmd {
uint8_t off;
std::array<uint8_t, 3> reserve;
};
struct BmsState {
uint8_t version_h;
uint8_t version_l;
uint8_t bms_status;
uint8_t SOC;
int32_t current;
uint16_t cycle;
std::array<int8_t, 2> BQ_NTC;
std::array<int8_t, 2> MCU_NTC;
std::array<uint16_t, 10> cell_vol;
};
struct Cartesian {
float x;
float y;
float z;
};
struct IMU {
std::array<float, 4> quaternion;
std::array<float, 3> gyroscope;
std::array<float, 3> accelerometer;
std::array<float, 3> rpy;
int8_t temperature;
};
struct LED {
uint8_t r;
uint8_t g;
uint8_t b;
};
struct MotorState {
uint8_t mode;
float q;
float dq;
float ddq;
float tauEst;
float q_raw;
float dq_raw;
float ddq_raw;
int8_t temperature;
std::array<uint32_t, 2> reserve;
};
struct MotorCmd {
uint8_t mode;
float q;
float dq;
float tau;
float Kp;
float Kd;
std::array<uint32_t, 3> reserve;
};
struct LowState {
std::array<uint8_t, 2> head;
uint8_t levelFlag;
uint8_t frameReserve;
std::array<uint32_t, 2> SN;
std::array<uint32_t, 2> version;
uint16_t bandWidth;
IMU imu;
std::array<MotorState, 20> motorState;
BmsState bms;
std::array<int16_t, 4> footForce;
std::array<int16_t, 4> footForceEst;
uint32_t tick;
std::array<uint8_t, 40> wirelessRemote;
uint32_t reserve;
uint32_t crc;
};
struct LowCmd {
std::array<uint8_t, 2> head;
uint8_t levelFlag;
uint8_t frameReserve;
std::array<uint32_t, 2> SN;
std::array<uint32_t, 2> version;
uint16_t bandWidth;
std::array<MotorCmd, 20> motorCmd;
BmsCmd bms;
std::array<uint8_t, 40> wirelessRemote;
uint32_t reserve;
uint32_t crc;
};
struct HighState {
std::array<uint8_t, 2> head;
uint8_t levelFlag;
uint8_t frameReserve;
std::array<uint32_t, 2> SN;
std::array<uint32_t, 2> version;
uint16_t bandWidth;
IMU imu;
std::array<MotorState, 20> motorState;
BmsState bms;
std::array<int16_t, 4> footForce;
std::array<int16_t, 4> footForceEst;
uint8_t mode;
float progress;
uint8_t gaitType;
float footRaiseHeight;
std::array<float, 3> position;
float bodyHeight;
std::array<float, 3> velocity;
float yawSpeed;
std::array<float, 4> rangeObstacle;
std::array<Cartesian, 4> footPosition2Body;
std::array<Cartesian, 4> footSpeed2Body;
std::array<uint8_t, 40> wirelessRemote;
uint32_t reserve;
uint32_t crc;
};
struct HighCmd {
std::array<uint8_t, 2> head;
uint8_t levelFlag;
uint8_t frameReserve;
std::array<uint32_t, 2> SN;
std::array<uint32_t, 2> version;
uint16_t bandWidth;
uint8_t mode;
uint8_t gaitType;
uint8_t speedLevel;
float footRaiseHeight;
float bodyHeight;
std::array<float, 2> position;
std::array<float, 3> euler;
std::array<float, 2> velocity;
float yawSpeed;
BmsCmd bms;
std::array<LED, 4> led;
std::array<uint8_t, 40> wirelessRemote;
uint32_t reserve;
uint32_t crc;
};
#pragma pack(pop)
struct UDPState {
unsigned long long TotalCount;
unsigned long long SendCount;
unsigned long long RecvCount;
unsigned long long SendError;
unsigned long long FlagError;
unsigned long long RecvCRCError;
unsigned long long RecvLoseError;
};
} // namespace UNITREE_LEGGED_SDK
#endif

View File

@@ -0,0 +1,11 @@
#ifndef GO1_PRO_UNITREE_LEGGED_GO1_CONST_H_
#define GO1_PRO_UNITREE_LEGGED_GO1_CONST_H_
namespace UNITREE_LEGGED_SDK {
constexpr double go1_Hip_max = 1.047;
constexpr double go1_Hip_min = -1.047;
constexpr double go1_Thigh_max = 2.966;
constexpr double go1_Thigh_min = -0.663;
constexpr double go1_Calf_max = -0.837;
constexpr double go1_Calf_min = -2.721;
} // namespace UNITREE_LEGGED_SDK
#endif

View File

@@ -0,0 +1,27 @@
#ifndef GO1_PRO_UNITREE_LEGGED_JOYSTICK_H_
#define GO1_PRO_UNITREE_LEGGED_JOYSTICK_H_
#include <cstdint>
typedef union {
struct {
uint8_t R1 : 1; uint8_t L1 : 1; uint8_t start : 1; uint8_t select : 1;
uint8_t R2 : 1; uint8_t L2 : 1; uint8_t F1 : 1; uint8_t F2 : 1;
uint8_t A : 1; uint8_t B : 1; uint8_t X : 1; uint8_t Y : 1;
uint8_t up : 1; uint8_t right : 1; uint8_t down : 1; uint8_t left : 1;
} components;
uint16_t value;
} xKeySwitchUnion;
typedef struct {
uint8_t head[2];
xKeySwitchUnion btn;
float lx;
float rx;
float ry;
float L2;
float ly;
uint8_t idle[16];
} xRockerBtnDataStruct;
#endif

View File

@@ -0,0 +1,48 @@
#ifndef GO1_PRO_UNITREE_LEGGED_LOOP_H_
#define GO1_PRO_UNITREE_LEGGED_LOOP_H_
#include <atomic>
#include <boost/function.hpp>
#include <string>
#include <thread>
namespace UNITREE_LEGGED_SDK {
constexpr int THREAD_PRIORITY = 95;
typedef boost::function<void()> Callback;
class Loop {
public:
Loop(std::string name, float period, int bindCPU = -1)
: _name(name), _period(period), _bindCPU(bindCPU) {}
virtual ~Loop();
void start();
void shutdown();
virtual void functionCB() = 0;
private:
void entryFunc();
std::string _name;
float _period;
int _bindCPU;
bool _bind_cpu_flag = false;
bool _isrunning = false;
std::atomic<bool> _running_atomic{false};
std::thread _thread;
};
class LoopFunc : public Loop {
public:
LoopFunc(std::string name, float period, const Callback& cb)
: Loop(name, period), _fp(cb) {}
LoopFunc(std::string name, float period, int bindCPU, const Callback& cb)
: Loop(name, period, bindCPU), _fp(cb) {}
void functionCB() override { _fp(); }
private:
boost::function<void()> _fp;
};
} // namespace UNITREE_LEGGED_SDK
#endif

View File

@@ -0,0 +1,33 @@
#ifndef GO1_PRO_UNITREE_LEGGED_QUADRUPED_H_
#define GO1_PRO_UNITREE_LEGGED_QUADRUPED_H_
#include <string>
namespace UNITREE_LEGGED_SDK {
enum class LeggedType { Aliengo, A1, Go1, B1 };
std::string VersionSDK();
int InitEnvironment();
constexpr int FR_ = 0;
constexpr int FL_ = 1;
constexpr int RR_ = 2;
constexpr int RL_ = 3;
constexpr int FR_0 = 0;
constexpr int FR_1 = 1;
constexpr int FR_2 = 2;
constexpr int FL_0 = 3;
constexpr int FL_1 = 4;
constexpr int FL_2 = 5;
constexpr int RR_0 = 6;
constexpr int RR_1 = 7;
constexpr int RR_2 = 8;
constexpr int RL_0 = 9;
constexpr int RL_1 = 10;
constexpr int RL_2 = 11;
} // namespace UNITREE_LEGGED_SDK
#endif

View File

@@ -0,0 +1,30 @@
#ifndef GO1_PRO_UNITREE_LEGGED_SAFETY_H_
#define GO1_PRO_UNITREE_LEGGED_SAFETY_H_
#include "comm.h"
#include "quadruped.h"
namespace UNITREE_LEGGED_SDK {
class Safety {
public:
Safety(LeggedType type);
~Safety();
void PositionLimit(LowCmd& cmd);
int PowerProtect(LowCmd& cmd, LowState& state, int factor);
int PositionProtect(LowCmd& cmd, LowState& state, double limit = 0.087);
private:
int WattLimit;
int Wcount;
double Hip_max;
double Hip_min;
double Thigh_max;
double Thigh_min;
double Calf_max;
double Calf_min;
};
} // namespace UNITREE_LEGGED_SDK
#endif

View File

@@ -0,0 +1,62 @@
#ifndef GO1_PRO_UNITREE_LEGGED_UDP_H_
#define GO1_PRO_UNITREE_LEGGED_UDP_H_
#include "comm.h"
#include "unitree_legged_sdk/quadruped.h"
#include <cstdint>
namespace UNITREE_LEGGED_SDK {
constexpr int UDP_CLIENT_PORT = 8080;
constexpr int UDP_SERVER_PORT = 8007;
constexpr char UDP_SERVER_IP_BASIC[] = "192.168.123.10";
constexpr char UDP_SERVER_IP_SPORT[] = "192.168.123.161";
enum RecvEnum { nonBlock = 0x00, block = 0x01, blockTimeout = 0x02 };
class UDP {
public:
UDP(uint8_t level, uint16_t localPort, const char* targetIP, uint16_t targetPort);
UDP(uint16_t localPort, const char* targetIP, uint16_t targetPort,
int sendLength, int recvLength, bool initiativeDisconnect = false,
RecvEnum recvType = RecvEnum::nonBlock);
UDP(uint16_t localPort, int sendLength, int recvLength,
bool initiativeDisconnect = false, RecvEnum recvType = RecvEnum::nonBlock,
bool setIpPort = false);
~UDP();
UDP(const UDP&) = delete;
UDP& operator=(const UDP&) = delete;
void SetIpPort(const char* targetIP, uint16_t targetPort);
void SetRecvTimeout(int time);
void SetDisconnectTime(float callback_dt, float disconnectTime);
void SetAccessibleTime(float callback_dt, float accessibleTime);
int Send();
int Recv();
void InitCmdData(HighCmd& cmd);
void InitCmdData(LowCmd& cmd);
int SetSend(char* data);
int SetSend(HighCmd& cmd);
int SetSend(LowCmd& cmd);
void GetRecv(char* data);
void GetRecv(HighState& state);
void GetRecv(LowState& state);
UDPState udpState;
char* targetIP;
uint16_t targetPort;
char* localIP;
uint16_t localPort;
bool accessible = false;
private:
struct Impl;
Impl* impl_;
};
} // namespace UNITREE_LEGGED_SDK
#endif

View File

@@ -0,0 +1,14 @@
#ifndef GO1_PRO_UNITREE_LEGGED_SDK_H_
#define GO1_PRO_UNITREE_LEGGED_SDK_H_
#include "comm.h"
#include "joystick.h"
#include "loop.h"
#include "quadruped.h"
#include "safety.h"
#include "udp.h"
#include <boost/bind.hpp>
#define UT UNITREE_LEGGED_SDK
#endif

View File

@@ -1,4 +1,3 @@
"""Drop-in import name for Unitree's official Python wrapper."""
from go1_pro_sdk.compat.robot_interface import *
from go1_pro_sdk.compat.robot_interface import __all__

50
src/loop.cpp Normal file
View File

@@ -0,0 +1,50 @@
#include "unitree_legged_sdk/loop.h"
#include <chrono>
#if defined(__linux__)
#include <pthread.h>
#include <sched.h>
#endif
namespace UNITREE_LEGGED_SDK {
Loop::~Loop() { shutdown(); }
void Loop::start() {
if (_running_atomic.exchange(true)) return;
_isrunning = true;
_thread = std::thread(&Loop::entryFunc, this);
}
void Loop::shutdown() {
_running_atomic.store(false);
_isrunning = false;
if (_thread.joinable()) _thread.join();
}
void Loop::entryFunc() {
#if defined(__linux__)
if (_bindCPU >= 0) {
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(_bindCPU, &cpu_set);
_bind_cpu_flag = pthread_setaffinity_np(
pthread_self(), sizeof(cpu_set), &cpu_set) == 0;
}
#else
(void)_bindCPU;
_bind_cpu_flag = false;
#endif
using Clock = std::chrono::steady_clock;
const auto period = std::chrono::duration_cast<Clock::duration>(
std::chrono::duration<double>(_period));
auto next = Clock::now();
while (_running_atomic.load()) {
next += period;
functionCB();
std::this_thread::sleep_until(next);
}
}
} // namespace UNITREE_LEGGED_SDK

391
src/pro_codec.cpp Normal file
View File

@@ -0,0 +1,391 @@
#include "pro_codec.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <limits.h>
#include <stdexcept>
#include <string>
#if defined(__APPLE__)
#include <mach-o/dyld.h>
#elif defined(__linux__)
#include <unistd.h>
#endif
#ifndef GO1_PRO_SOURCE_STATE_FILE
#define GO1_PRO_SOURCE_STATE_FILE ""
#endif
#ifndef GO1_PRO_INSTALL_STATE_FILE
#define GO1_PRO_INSTALL_STATE_FILE ""
#endif
namespace go1_pro_internal {
namespace {
constexpr std::size_t kStateSize = 4168;
constexpr std::size_t kMotorOffset = 22;
constexpr std::size_t kMotorWireSize = 27;
constexpr std::size_t kCrcOffset = 612;
constexpr uint32_t kCrcPoly = 0x04c11db7u;
uint16_t GetU16Le(const uint8_t* p) {
return static_cast<uint16_t>(p[0]) |
static_cast<uint16_t>(static_cast<uint16_t>(p[1]) << 8);
}
int16_t GetI16Le(const uint8_t* p) {
return static_cast<int16_t>(GetU16Le(p));
}
uint32_t GetU32Le(const uint8_t* p) {
return static_cast<uint32_t>(p[0]) |
(static_cast<uint32_t>(p[1]) << 8) |
(static_cast<uint32_t>(p[2]) << 16) |
(static_cast<uint32_t>(p[3]) << 24);
}
int32_t GetI32Le(const uint8_t* p) {
return static_cast<int32_t>(GetU32Le(p));
}
float GetF32Le(const uint8_t* p) {
const uint32_t raw = GetU32Le(p);
float result;
std::memcpy(&result, &raw, sizeof(result));
return result;
}
void PutU16Le(uint8_t* p, uint16_t value) {
p[0] = static_cast<uint8_t>(value);
p[1] = static_cast<uint8_t>(value >> 8);
}
void PutU32Le(uint8_t* p, uint32_t value) {
p[0] = static_cast<uint8_t>(value);
p[1] = static_cast<uint8_t>(value >> 8);
p[2] = static_cast<uint8_t>(value >> 16);
p[3] = static_cast<uint8_t>(value >> 24);
}
void PutF32Le(uint8_t* p, float value) {
uint32_t raw;
static_assert(sizeof(raw) == sizeof(value), "float must be 32-bit");
std::memcpy(&raw, &value, sizeof(raw));
PutU32Le(p, raw);
}
double RoundHalfEven(double value, double scale) {
const double scaled = value * scale;
const double floor_value = std::floor(scaled);
const double fraction = scaled - floor_value;
constexpr double kEpsilon = 1e-12;
double rounded = floor_value;
if (fraction > 0.5 + kEpsilon) {
rounded += 1.0;
} else if (fraction >= 0.5 - kEpsilon && std::fmod(floor_value, 2.0) != 0.0) {
rounded += 1.0;
}
return rounded / scale;
}
void EncodeTau(uint8_t* p, double tau) {
tau = RoundHalfEven(tau, 100.0);
int integer = static_cast<int>(tau);
const double fraction = tau - static_cast<double>(integer);
bool negative = tau < 0.0;
if (negative) integer = 255 + integer;
if (fraction == 0.0) negative = false;
int encoded_fraction = static_cast<int>(fraction * 256.0);
if (negative) encoded_fraction = 256 + encoded_fraction;
p[0] = static_cast<uint8_t>(encoded_fraction);
p[1] = static_cast<uint8_t>(integer);
}
void EncodeKp(uint8_t* p, double kp) {
const double base_double = std::floor(kp);
const int base = static_cast<int>(base_double);
const int fraction = static_cast<int>(RoundHalfEven(kp - base_double, 10.0) * 10.0);
const int value = fraction < 5 ? base * 32 + fraction * 3
: base * 32 + (fraction - 1) * 3 + 4;
PutU16Le(p, static_cast<uint16_t>(value));
}
void EncodeKd(uint8_t* p, double kd) {
static constexpr uint8_t kFraction[10] = {0x0, 0x1, 0x3, 0x4, 0x6,
0x8, 0x9, 0xb, 0xc, 0xe};
const int integer = static_cast<int>(kd);
const int fraction = static_cast<int>(RoundHalfEven(kd - integer, 10.0) * 10.0);
const uint8_t nibble = fraction >= 0 && fraction < 10 ? kFraction[fraction] : 0;
PutU16Le(p, static_cast<uint16_t>((integer << 4) | nibble));
}
void EncodeMotor(uint8_t* p, const UNITREE_LEGGED_SDK::MotorCmd& motor) {
p[0] = motor.mode;
PutF32Le(p + 1, motor.q);
PutF32Le(p + 5, motor.dq);
EncodeTau(p + 9, motor.tau);
EncodeKp(p + 11, motor.Kp);
EncodeKd(p + 13, motor.Kd);
for (std::size_t i = 0; i < motor.reserve.size(); ++i) {
PutU32Le(p + 15 + i * 4, motor.reserve[i]);
}
}
bool ReadableFile(const std::string& path) {
if (path.empty()) return false;
std::ifstream stream(path, std::ios::binary);
return stream.good();
}
std::string ExecutableDirectory() {
#if defined(__APPLE__)
uint32_t size = 0;
_NSGetExecutablePath(nullptr, &size);
std::string path(size, '\0');
if (_NSGetExecutablePath(&path[0], &size) != 0) return {};
path.resize(std::strlen(path.c_str()));
#elif defined(__linux__)
char buffer[PATH_MAX] = {};
const ssize_t size = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1);
if (size <= 0) return {};
buffer[size] = '\0';
std::string path(buffer);
#else
return {};
#endif
const std::size_t slash = path.find_last_of("/");
return slash == std::string::npos ? std::string{} : path.substr(0, slash);
}
} // namespace
void InitLowCmd(UNITREE_LEGGED_SDK::LowCmd& cmd) {
std::memset(&cmd, 0, sizeof(cmd));
cmd.head = {{0xfe, 0xef}};
cmd.levelFlag = UNITREE_LEGGED_SDK::LOWLEVEL;
cmd.bandWidth = 0x3ac0;
for (auto& motor : cmd.motorCmd) {
motor.mode = 0x0a;
motor.q = static_cast<float>(UNITREE_LEGGED_SDK::PosStopF);
motor.dq = static_cast<float>(UNITREE_LEGGED_SDK::VelStopF);
}
}
void InitHighCmd(UNITREE_LEGGED_SDK::HighCmd& cmd) {
std::memset(&cmd, 0, sizeof(cmd));
cmd.head = {{0xfe, 0xef}};
cmd.levelFlag = UNITREE_LEGGED_SDK::HIGHLEVEL;
}
ProCodec::ProCodec(const std::string& state_path) {
std::ifstream stream(state_path, std::ios::binary);
if (!stream) {
throw std::runtime_error("cannot open Go1 PRO Blowfish state file: " + state_path);
}
std::array<uint8_t, kStateSize> data{};
stream.read(reinterpret_cast<char*>(data.data()), data.size());
if (stream.gcount() != static_cast<std::streamsize>(data.size())) {
throw std::runtime_error("Go1 PRO Blowfish state file must contain at least 4168 bytes: " + state_path);
}
for (std::size_t i = 0; i < p_.size(); ++i) p_[i] = GetU32Le(data.data() + i * 4);
const uint8_t* boxes = data.data() + p_.size() * 4;
for (std::size_t box = 0; box < s_.size(); ++box) {
for (std::size_t i = 0; i < s_[box].size(); ++i) {
s_[box][i] = GetU32Le(boxes + (box * 256 + i) * 4);
}
}
}
std::string ProCodec::FindStateFile() {
const char* environment = std::getenv("GO1_PRO_BLOWFISH_STATE");
const std::string executable_dir = ExecutableDirectory();
const std::string candidates[] = {
environment ? environment : "",
GO1_PRO_SOURCE_STATE_FILE,
GO1_PRO_INSTALL_STATE_FILE,
executable_dir.empty() ? "" : executable_dir + "/../share/go1_pro_sdk/blowfish_state.bin",
executable_dir.empty() ? "" : executable_dir + "/share/go1_pro_sdk/blowfish_state.bin",
"go1_pro_sdk/_data/blowfish_state.bin",
"../share/go1_pro_sdk/blowfish_state.bin",
"/usr/local/share/go1_pro_sdk/blowfish_state.bin",
};
for (const auto& candidate : candidates) {
if (ReadableFile(candidate)) return candidate;
}
throw std::runtime_error(
"Go1 PRO Blowfish state not found; set GO1_PRO_BLOWFISH_STATE to blowfish_state.bin");
}
uint32_t ProCodec::Crc32(const uint8_t* data, std::size_t size) {
static const std::array<uint32_t, 256> table = [] {
std::array<uint32_t, 256> result{};
for (uint32_t byte = 0; byte < 256; ++byte) {
uint32_t crc = byte << 24;
for (int bit = 0; bit < 8; ++bit) {
crc = (crc & 0x80000000u) ? (crc << 1) ^ kCrcPoly : crc << 1;
}
result[byte] = crc;
}
return result;
}();
uint32_t crc = 0xffffffffu;
for (std::size_t offset = 0; offset + 4 <= size; offset += 4) {
const uint32_t word = GetU32Le(data + offset);
for (int shift = 24; shift >= 0; shift -= 8) {
crc = (crc << 8) ^ table[((crc >> 24) ^ (word >> shift)) & 0xffu];
}
}
return crc;
}
uint32_t ProCodec::F(uint32_t value) const {
return ((s_[0][(value >> 24) & 0xffu] + s_[1][(value >> 16) & 0xffu]) ^
s_[2][(value >> 8) & 0xffu]) + s_[3][value & 0xffu];
}
void ProCodec::EncryptBlock(const uint8_t* input, uint8_t* output) const {
uint32_t left = GetU32Le(input);
uint32_t right = GetU32Le(input + 4);
for (int i = 0; i < 16; ++i) {
left ^= p_[i];
right ^= F(left);
const uint32_t temporary = left; left = right; right = temporary;
}
const uint32_t temporary = left; left = right; right = temporary;
right ^= p_[16];
left ^= p_[17];
PutU32Le(output, left);
PutU32Le(output + 4, right);
}
void ProCodec::DecryptBlock(const uint8_t* input, uint8_t* output) const {
uint32_t left = GetU32Le(input);
uint32_t right = GetU32Le(input + 4);
for (int i = 17; i > 1; --i) {
left ^= p_[i];
right ^= F(left);
const uint32_t temporary = left; left = right; right = temporary;
}
const uint32_t temporary = left; left = right; right = temporary;
right ^= p_[1];
left ^= p_[0];
PutU32Le(output, left);
PutU32Le(output + 4, right);
}
void ProCodec::Encrypt(const uint8_t* input, uint8_t* output, std::size_t size) const {
if (size % 8 != 0) throw std::invalid_argument("Blowfish input must be 8-byte aligned");
for (std::size_t offset = 0; offset < size; offset += 8) {
EncryptBlock(input + offset, output + offset);
}
}
void ProCodec::Decrypt(const uint8_t* input, uint8_t* output, std::size_t size) const {
if (size % 8 != 0) throw std::invalid_argument("Blowfish input must be 8-byte aligned");
for (std::size_t offset = 0; offset < size; offset += 8) {
DecryptBlock(input + offset, output + offset);
}
}
std::array<uint8_t, kLowCmdWireSize> ProCodec::EncodeLowCmd(
const UNITREE_LEGGED_SDK::LowCmd& cmd) const {
std::array<uint8_t, kLowCmdWireSize> plain{};
plain[0] = cmd.head[0];
plain[1] = cmd.head[1];
plain[2] = cmd.levelFlag;
plain[3] = cmd.frameReserve;
for (std::size_t i = 0; i < 2; ++i) {
PutU32Le(plain.data() + 4 + i * 4, cmd.SN[i]);
PutU32Le(plain.data() + 12 + i * 4, cmd.version[i]);
}
plain[20] = static_cast<uint8_t>(cmd.bandWidth >> 8);
plain[21] = static_cast<uint8_t>(cmd.bandWidth);
for (std::size_t i = 0; i < cmd.motorCmd.size(); ++i) {
EncodeMotor(plain.data() + kMotorOffset + i * kMotorWireSize, cmd.motorCmd[i]);
}
plain[562] = cmd.bms.off;
std::copy(cmd.bms.reserve.begin(), cmd.bms.reserve.end(), plain.begin() + 563);
std::copy(cmd.wirelessRemote.begin(), cmd.wirelessRemote.end(), plain.begin() + 566);
PutU32Le(plain.data() + 606, cmd.reserve);
PutU32Le(plain.data() + kCrcOffset, Crc32(plain.data(), kCrcOffset));
std::array<uint8_t, kLowCmdWireSize> encrypted{};
Encrypt(plain.data(), encrypted.data(), encrypted.size());
return encrypted;
}
bool ProCodec::DecodeLowState(const uint8_t* encrypted, std::size_t size,
UNITREE_LEGGED_SDK::LowState& state) const {
if (size < kLowStateParsedSize) return false;
const std::size_t aligned = (size / 8) * 8;
if (aligned < kLowStateParsedSize) return false;
std::vector<uint8_t> data(aligned);
Decrypt(encrypted, data.data(), aligned);
if (data[0] != 0xfe || data[1] != 0xef ||
data[2] != UNITREE_LEGGED_SDK::LOWLEVEL || data[3] != 0) {
return false;
}
std::memset(&state, 0, sizeof(state));
state.head = {{data[0], data[1]}};
state.levelFlag = data[2];
state.frameReserve = data[3];
for (std::size_t i = 0; i < 2; ++i) {
state.SN[i] = GetU32Le(data.data() + 4 + i * 4);
state.version[i] = GetU32Le(data.data() + 12 + i * 4);
}
state.bandWidth = GetU16Le(data.data() + 20);
for (std::size_t i = 0; i < 4; ++i) state.imu.quaternion[i] = GetF32Le(data.data() + 22 + i * 4);
for (std::size_t i = 0; i < 3; ++i) {
state.imu.gyroscope[i] = GetF32Le(data.data() + 38 + i * 4);
state.imu.accelerometer[i] = GetF32Le(data.data() + 50 + i * 4);
state.imu.rpy[i] = GetF32Le(data.data() + 62 + i * 4);
}
state.imu.temperature = static_cast<int8_t>(data[74]);
for (std::size_t i = 0; i < state.motorState.size(); ++i) {
const uint8_t* motor = data.data() + 75 + i * 32;
auto& output = state.motorState[i];
output.mode = motor[0];
output.q = GetF32Le(motor + 1);
output.dq = GetF32Le(motor + 5);
output.ddq = static_cast<float>(GetI16Le(motor + 9));
output.tauEst = static_cast<float>(GetI16Le(motor + 11)) * 0.00390625f;
output.q_raw = GetF32Le(motor + 13);
output.dq_raw = GetF32Le(motor + 17);
output.ddq_raw = static_cast<float>(GetI16Le(motor + 21));
output.temperature = static_cast<int8_t>(motor[23]);
output.reserve[0] = GetU32Le(motor + 24);
output.reserve[1] = GetU32Le(motor + 28);
}
const uint8_t* bms = data.data() + 715;
state.bms.version_h = bms[0];
state.bms.version_l = bms[1];
state.bms.bms_status = bms[2];
state.bms.SOC = bms[3];
state.bms.current = GetI32Le(bms + 4);
state.bms.cycle = GetU16Le(bms + 8);
state.bms.BQ_NTC = {{static_cast<int8_t>(bms[10]), static_cast<int8_t>(bms[11])}};
state.bms.MCU_NTC = {{static_cast<int8_t>(bms[12]), static_cast<int8_t>(bms[13])}};
for (std::size_t i = 0; i < 5; ++i) state.bms.cell_vol[i] = GetU16Le(bms + 14 + i * 2);
// The PRO state packs these fields differently from the public EDU struct.
// Keep the reverse-engineered mapping used by the Python implementation.
const std::size_t force_offsets[4] = {739, 751, 753, 755};
const std::size_t force_est_offsets[4] = {747, 759, 761, 763};
for (std::size_t i = 0; i < 4; ++i) {
state.footForce[i] = GetI16Le(data.data() + force_offsets[i]);
state.footForceEst[i] = GetI16Le(data.data() + force_est_offsets[i]);
}
std::copy(data.begin() + 759, data.begin() + 799, state.wirelessRemote.begin());
state.reserve = GetU32Le(data.data() + 799);
state.crc = GetU32Le(data.data() + 803);
return true;
}
} // namespace go1_pro_internal

48
src/pro_codec.h Normal file
View File

@@ -0,0 +1,48 @@
#ifndef GO1_PRO_SDK_PRO_CODEC_H_
#define GO1_PRO_SDK_PRO_CODEC_H_
#include "unitree_legged_sdk/comm.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace go1_pro_internal {
constexpr std::size_t kLowCmdWireSize = 616;
constexpr std::size_t kLowStateDatagramSize = 858;
constexpr std::size_t kLowStateEncryptedSize = 856;
constexpr std::size_t kLowStateParsedSize = 807;
void InitLowCmd(UNITREE_LEGGED_SDK::LowCmd& cmd);
void InitHighCmd(UNITREE_LEGGED_SDK::HighCmd& cmd);
class ProCodec {
public:
explicit ProCodec(const std::string& state_path);
static std::string FindStateFile();
static uint32_t Crc32(const uint8_t* data, std::size_t size);
std::array<uint8_t, kLowCmdWireSize> EncodeLowCmd(
const UNITREE_LEGGED_SDK::LowCmd& cmd) const;
bool DecodeLowState(const uint8_t* encrypted, std::size_t size,
UNITREE_LEGGED_SDK::LowState& state) const;
void Encrypt(const uint8_t* input, uint8_t* output, std::size_t size) const;
void Decrypt(const uint8_t* input, uint8_t* output, std::size_t size) const;
private:
uint32_t F(uint32_t value) const;
void EncryptBlock(const uint8_t* input, uint8_t* output) const;
void DecryptBlock(const uint8_t* input, uint8_t* output) const;
std::array<uint32_t, 18> p_{};
std::array<std::array<uint32_t, 256>, 4> s_{};
};
} // namespace go1_pro_internal
#endif

25
src/quadruped.cpp Normal file
View File

@@ -0,0 +1,25 @@
#include "unitree_legged_sdk/quadruped.h"
#include "unitree_legged_sdk/comm.h"
#if defined(__unix__) || defined(__APPLE__)
#include <sys/mman.h>
#endif
namespace UNITREE_LEGGED_SDK {
const int HIGH_CMD_LENGTH = sizeof(HighCmd);
const int HIGH_STATE_LENGTH = sizeof(HighState);
const int LOW_CMD_LENGTH = 616;
const int LOW_STATE_LENGTH = 858;
std::string VersionSDK() { return "go1-pro-compat-3.8.6"; }
int InitEnvironment() {
#if defined(__unix__) || defined(__APPLE__)
return mlockall(MCL_CURRENT | MCL_FUTURE);
#else
return 0;
#endif
}
} // namespace UNITREE_LEGGED_SDK

77
src/safety.cpp Normal file
View File

@@ -0,0 +1,77 @@
#include "unitree_legged_sdk/safety.h"
#include <algorithm>
#include <cmath>
namespace UNITREE_LEGGED_SDK {
namespace {
constexpr double kTorqueLimit[12] = {
23.7, 23.7, 35.55, 23.7, 23.7, 35.55,
23.7, 23.7, 35.55, 23.7, 23.7, 35.55,
};
bool IsPositionStop(float value) {
return std::fabs(static_cast<double>(value)) >= PosStopF * 0.1;
}
} // namespace
Safety::Safety(LeggedType type)
: WattLimit(0), Wcount(0),
Hip_max(0.78), Hip_min(-0.78),
Thigh_max(3.50), Thigh_min(-0.60),
Calf_max(-0.95), Calf_min(-2.70) {
(void)type;
}
Safety::~Safety() = default;
void Safety::PositionLimit(LowCmd& cmd) {
for (std::size_t i = 0; i < 12; ++i) {
auto& motor = cmd.motorCmd[i];
if (IsPositionStop(motor.q)) continue;
const std::size_t joint = i % 3;
const double minimum = joint == 0 ? Hip_min : (joint == 1 ? Thigh_min : Calf_min);
const double maximum = joint == 0 ? Hip_max : (joint == 1 ? Thigh_max : Calf_max);
motor.q = static_cast<float>(std::max(minimum, std::min(maximum, static_cast<double>(motor.q))));
}
}
int Safety::PowerProtect(LowCmd& cmd, LowState& state, int factor) {
if (factor < 1 || factor > 10) return -1;
WattLimit = factor;
int limited = 0;
for (std::size_t i = 0; i < 12; ++i) {
const double maximum = kTorqueLimit[i];
if (std::fabs(static_cast<double>(cmd.motorCmd[i].tau)) > maximum * 5.0 ||
std::fabs(static_cast<double>(state.motorState[i].tauEst)) > maximum) {
return -1;
}
const double limit = maximum * static_cast<double>(factor) / 10.0;
const float bounded = static_cast<float>(
std::max(-limit, std::min(limit, static_cast<double>(cmd.motorCmd[i].tau))));
if (bounded != cmd.motorCmd[i].tau) {
cmd.motorCmd[i].tau = bounded;
++limited;
}
}
Wcount += limited;
return limited;
}
int Safety::PositionProtect(LowCmd& cmd, LowState& state, double limit) {
int protected_count = 0;
for (std::size_t i = 0; i < 12; ++i) {
auto& motor = cmd.motorCmd[i];
if (IsPositionStop(motor.q)) continue;
if (std::fabs(static_cast<double>(motor.q - state.motorState[i].q)) > limit) {
motor.Kp = 0.0f;
motor.Kd = 0.0f;
++protected_count;
}
}
return protected_count;
}
} // namespace UNITREE_LEGGED_SDK

310
src/udp.cpp Normal file
View File

@@ -0,0 +1,310 @@
#include "unitree_legged_sdk/udp.h"
#include "pro_codec.h"
#include <algorithm>
#include <arpa/inet.h>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <memory>
#include <mutex>
#include <netinet/in.h>
#include <stdexcept>
#include <string>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include <vector>
namespace UNITREE_LEGGED_SDK {
namespace {
char* Duplicate(const char* value) {
if (!value) return nullptr;
const std::size_t size = std::strlen(value) + 1;
char* result = new char[size];
std::memcpy(result, value, size);
return result;
}
std::runtime_error SocketError(const char* operation) {
return std::runtime_error(std::string(operation) + ": " + std::strerror(errno));
}
} // namespace
struct UDP::Impl {
int socket_fd = -1;
int send_length = 0;
int recv_length = 0;
RecvEnum recv_type = RecvEnum::nonBlock;
bool pro_low_level = false;
bool target_configured = false;
std::mutex send_mutex;
std::mutex recv_mutex;
std::mutex socket_recv_mutex;
std::mutex state_mutex;
std::unique_ptr<go1_pro_internal::ProCodec> codec;
std::vector<uint8_t> send_buffer;
std::vector<uint8_t> recv_buffer;
std::vector<uint8_t> wire_recv_buffer;
std::size_t received_size = 0;
LowState low_state{};
bool have_low_state = false;
};
namespace {
template <typename ImplType>
void ConfigureBlocking(ImplType& impl) {
const int flags = fcntl(impl.socket_fd, F_GETFL, 0);
if (flags < 0) throw SocketError("fcntl(F_GETFL)");
const int next = impl.recv_type == RecvEnum::nonBlock ? flags | O_NONBLOCK : flags & ~O_NONBLOCK;
if (fcntl(impl.socket_fd, F_SETFL, next) < 0) throw SocketError("fcntl(F_SETFL)");
}
template <typename ImplType>
void BindSocket(UDP& udp, ImplType& impl, uint16_t port) {
impl.socket_fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (impl.socket_fd < 0) throw SocketError("socket");
const int reuse = 1;
setsockopt(impl.socket_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
sockaddr_in local{};
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (::bind(impl.socket_fd, reinterpret_cast<sockaddr*>(&local), sizeof(local)) < 0) {
throw SocketError("bind");
}
udp.localPort = port;
udp.localIP = Duplicate("0.0.0.0");
ConfigureBlocking(impl);
}
} // namespace
UDP::UDP(uint8_t level, uint16_t port, const char* ip, uint16_t target_port)
: udpState{}, targetIP(nullptr), targetPort(0), localIP(nullptr), localPort(0),
impl_(new Impl) {
try {
if (level != LOWLEVEL) {
throw std::invalid_argument("Go1 PRO compatibility library supports LOWLEVEL UDP only");
}
impl_->pro_low_level = true;
impl_->send_length = static_cast<int>(go1_pro_internal::kLowCmdWireSize);
impl_->recv_length = static_cast<int>(go1_pro_internal::kLowStateDatagramSize);
impl_->send_buffer.resize(impl_->send_length);
impl_->recv_buffer.resize(2048);
impl_->wire_recv_buffer.resize(2048);
impl_->codec.reset(new go1_pro_internal::ProCodec(
go1_pro_internal::ProCodec::FindStateFile()));
BindSocket(*this, *impl_, port);
SetIpPort(ip, target_port);
} catch (...) {
if (impl_->socket_fd >= 0) ::close(impl_->socket_fd);
delete[] localIP;
delete impl_;
throw;
}
}
UDP::UDP(uint16_t port, const char* ip, uint16_t target_port,
int send_length, int recv_length, bool initiative_disconnect, RecvEnum recv_type)
: udpState{}, targetIP(nullptr), targetPort(0), localIP(nullptr), localPort(0),
impl_(new Impl) {
(void)initiative_disconnect;
try {
impl_->send_length = send_length;
impl_->recv_length = recv_length;
impl_->recv_type = recv_type;
impl_->send_buffer.resize(send_length);
impl_->recv_buffer.resize(recv_length);
impl_->wire_recv_buffer.resize(recv_length);
BindSocket(*this, *impl_, port);
SetIpPort(ip, target_port);
} catch (...) {
if (impl_->socket_fd >= 0) ::close(impl_->socket_fd);
delete[] localIP;
delete impl_;
throw;
}
}
UDP::UDP(uint16_t port, int send_length, int recv_length,
bool initiative_disconnect, RecvEnum recv_type, bool set_ip_port)
: udpState{}, targetIP(nullptr), targetPort(0), localIP(nullptr), localPort(0),
impl_(new Impl) {
(void)initiative_disconnect;
(void)set_ip_port;
try {
impl_->send_length = send_length;
impl_->recv_length = recv_length;
impl_->recv_type = recv_type;
impl_->send_buffer.resize(send_length);
impl_->recv_buffer.resize(recv_length);
impl_->wire_recv_buffer.resize(recv_length);
BindSocket(*this, *impl_, port);
} catch (...) {
if (impl_->socket_fd >= 0) ::close(impl_->socket_fd);
delete[] localIP;
delete impl_;
throw;
}
}
UDP::~UDP() {
if (impl_) {
if (impl_->socket_fd >= 0) ::close(impl_->socket_fd);
delete impl_;
}
delete[] targetIP;
delete[] localIP;
}
void UDP::SetIpPort(const char* ip, uint16_t port) {
if (!ip) throw std::invalid_argument("targetIP must not be null");
sockaddr_in target{};
target.sin_family = AF_INET;
target.sin_port = htons(port);
if (inet_pton(AF_INET, ip, &target.sin_addr) != 1) {
throw std::invalid_argument(std::string("invalid IPv4 target: ") + ip);
}
if (::connect(impl_->socket_fd, reinterpret_cast<sockaddr*>(&target), sizeof(target)) < 0) {
throw SocketError("connect");
}
delete[] targetIP;
targetIP = Duplicate(ip);
targetPort = port;
impl_->target_configured = true;
}
void UDP::SetRecvTimeout(int time) {
timeval timeout{};
timeout.tv_sec = time / 1000;
timeout.tv_usec = (time % 1000) * 1000;
if (setsockopt(impl_->socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
throw SocketError("setsockopt(SO_RCVTIMEO)");
}
impl_->recv_type = RecvEnum::blockTimeout;
ConfigureBlocking(*impl_);
}
void UDP::SetDisconnectTime(float callback_dt, float disconnectTime) {
(void)callback_dt;
(void)disconnectTime;
}
void UDP::SetAccessibleTime(float callback_dt, float accessibleTime) {
(void)callback_dt;
(void)accessibleTime;
}
int UDP::Send() {
{
std::lock_guard<std::mutex> state_lock(impl_->state_mutex);
++udpState.TotalCount;
}
std::lock_guard<std::mutex> lock(impl_->send_mutex);
if (!impl_->target_configured || impl_->send_buffer.empty()) {
std::lock_guard<std::mutex> state_lock(impl_->state_mutex);
++udpState.SendError;
return -1;
}
const ssize_t sent = ::send(impl_->socket_fd, impl_->send_buffer.data(),
impl_->send_buffer.size(), 0);
if (sent < 0) {
std::lock_guard<std::mutex> state_lock(impl_->state_mutex);
++udpState.SendError;
return -1;
}
{
std::lock_guard<std::mutex> state_lock(impl_->state_mutex);
++udpState.SendCount;
}
return static_cast<int>(sent);
}
int UDP::Recv() {
{
std::lock_guard<std::mutex> state_lock(impl_->state_mutex);
++udpState.TotalCount;
}
std::lock_guard<std::mutex> socket_lock(impl_->socket_recv_mutex);
const ssize_t received = ::recv(impl_->socket_fd, impl_->wire_recv_buffer.data(),
impl_->wire_recv_buffer.size(), 0);
if (received < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return -1;
return -1;
}
LowState decoded{};
if (impl_->pro_low_level) {
if (!impl_->codec->DecodeLowState(impl_->wire_recv_buffer.data(),
static_cast<std::size_t>(received), decoded)) {
std::lock_guard<std::mutex> state_lock(impl_->state_mutex);
++udpState.FlagError;
return -1;
}
}
{
std::lock_guard<std::mutex> lock(impl_->recv_mutex);
++udpState.TotalCount;
impl_->received_size = static_cast<std::size_t>(received);
impl_->recv_buffer.assign(impl_->wire_recv_buffer.begin(),
impl_->wire_recv_buffer.begin() + received);
if (impl_->pro_low_level) {
impl_->low_state = decoded;
impl_->have_low_state = true;
}
accessible = true;
}
{
std::lock_guard<std::mutex> state_lock(impl_->state_mutex);
++udpState.RecvCount;
}
return static_cast<int>(received);
}
void UDP::InitCmdData(LowCmd& cmd) {
go1_pro_internal::InitLowCmd(cmd);
}
void UDP::InitCmdData(HighCmd& cmd) {
go1_pro_internal::InitHighCmd(cmd);
}
int UDP::SetSend(char* data) {
if (!data) return -1;
std::lock_guard<std::mutex> lock(impl_->send_mutex);
std::memcpy(impl_->send_buffer.data(), data, impl_->send_buffer.size());
return 0;
}
int UDP::SetSend(LowCmd& cmd) {
if (!impl_->pro_low_level || !impl_->codec) return -1;
const auto encoded = impl_->codec->EncodeLowCmd(cmd);
std::lock_guard<std::mutex> lock(impl_->send_mutex);
impl_->send_buffer.assign(encoded.begin(), encoded.end());
return 0;
}
int UDP::SetSend(HighCmd& cmd) {
(void)cmd;
return -1;
}
void UDP::GetRecv(char* data) {
std::lock_guard<std::mutex> lock(impl_->recv_mutex);
if (!data || impl_->received_size == 0) return;
std::memcpy(data, impl_->recv_buffer.data(),
std::min(impl_->received_size, static_cast<std::size_t>(impl_->recv_length)));
}
void UDP::GetRecv(LowState& state) {
std::lock_guard<std::mutex> lock(impl_->recv_mutex);
if (impl_->have_low_state) state = impl_->low_state;
}
void UDP::GetRecv(HighState& state) { (void)state; }
} // namespace UNITREE_LEGGED_SDK

View File

@@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.16)
project(unitree_legged_sdk_package_consumer LANGUAGES CXX)
find_package(unitree_legged_sdk 3.8 CONFIG REQUIRED)
add_executable(package_consumer main.cpp)
target_link_libraries(package_consumer PRIVATE unitree_legged_sdk::unitree_legged_sdk)

View File

@@ -0,0 +1,9 @@
#include "unitree_legged_sdk/unitree_legged_sdk.h"
#include <string>
int main() {
UNITREE_LEGGED_SDK::LowCmd command{};
command.motorCmd[UNITREE_LEGGED_SDK::FR_0].q = 0.0f;
return UNITREE_LEGGED_SDK::VersionSDK().empty() ? 1 : 0;
}

127
tests/cpp/test_compat.cpp Normal file
View File

@@ -0,0 +1,127 @@
#include "pro_codec.h"
#include "unitree_legged_sdk/a1_const.h"
#include "unitree_legged_sdk/aliengo_const.h"
#include "unitree_legged_sdk/b1_const.h"
#include "unitree_legged_sdk/go1_const.h"
#include "unitree_legged_sdk/unitree_legged_sdk.h"
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <fstream>
#include <iterator>
#include <string>
#include <thread>
#include <type_traits>
#include <vector>
using namespace UNITREE_LEGGED_SDK;
namespace {
#define CHECK(expression) do { if (!(expression)) return __LINE__; } while (false)
uint64_t Fnv1a(const uint8_t* data, std::size_t size) {
uint64_t result = 1469598103934665603ull;
for (std::size_t i = 0; i < size; ++i) {
result ^= data[i];
result *= 1099511628211ull;
}
return result;
}
std::vector<uint8_t> ReadFile(const std::string& path) {
std::ifstream stream(path, std::ios::binary);
return std::vector<uint8_t>(
(std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
}
} // namespace
int main() {
static_assert(std::is_same<decltype(LowCmd::motorCmd), std::array<MotorCmd, 20>>::value,
"official LowCmd field shape changed");
static_assert(sizeof(IMU) == 53, "IMU must match official packed declaration");
static_assert(sizeof(MotorCmd) == 33, "MotorCmd must match official packed declaration");
static_assert(sizeof(MotorState) == 38, "MotorState must match official packed declaration");
static_assert(sizeof(BmsState) == 34, "BmsState must match official packed declaration");
static_assert(sizeof(LowCmd) == 734, "LowCmd must match official packed declaration");
static_assert(sizeof(LowState) == 937, "LowState must match official packed declaration");
static_assert(sizeof(HighCmd) == 129, "HighCmd must match official packed declaration");
static_assert(sizeof(HighState) == 1087, "HighState must match official packed declaration");
static_assert(go1_Hip_max > 1.0 && a1_Hip_max < 1.0 &&
aliengo_Hip_max > 1.0 && b1_Hip_max < 1.0,
"official robot constant headers must be available");
go1_pro_internal::ProCodec codec(go1_pro_internal::ProCodec::FindStateFile());
LowCmd initialized{};
go1_pro_internal::InitLowCmd(initialized);
CHECK(initialized.head[0] == 0xfe && initialized.head[1] == 0xef);
CHECK(initialized.levelFlag == LOWLEVEL && initialized.bandWidth == 0x3ac0);
for (const auto& motor : initialized.motorCmd) {
CHECK(motor.mode == 0x0a);
CHECK(motor.q == static_cast<float>(PosStopF));
CHECK(motor.dq == static_cast<float>(VelStopF));
CHECK(motor.Kp == 0.0f && motor.Kd == 0.0f && motor.tau == 0.0f);
}
std::array<uint8_t, 8> zero{};
std::array<uint8_t, 8> encrypted{};
codec.Encrypt(zero.data(), encrypted.data(), encrypted.size());
const std::array<uint8_t, 8> known = {{0x12, 0xaa, 0x03, 0x54, 0x23, 0x6b, 0x66, 0xe3}};
CHECK(encrypted == known);
LowCmd cmd{};
cmd.head = {{0xfe, 0xef}};
cmd.levelFlag = LOWLEVEL;
cmd.bandWidth = 0x3ac0;
const auto packet = codec.EncodeLowCmd(cmd);
CHECK(Fnv1a(packet.data(), packet.size()) == 0x63c5ef1a61977e18ull);
#if GO1_PRO_HAVE_CAPTURE_FIXTURES
const auto real_lowcmd = ReadFile(GO1_PRO_TEST_LOWCMD);
const auto real_lowcmd_plain = ReadFile(GO1_PRO_TEST_LOWCMD_PLAIN);
CHECK(real_lowcmd.size() == go1_pro_internal::kLowCmdWireSize);
CHECK(real_lowcmd_plain.size() == go1_pro_internal::kLowCmdWireSize);
std::vector<uint8_t> decrypted_lowcmd(real_lowcmd.size());
codec.Decrypt(real_lowcmd.data(), decrypted_lowcmd.data(), real_lowcmd.size());
CHECK(decrypted_lowcmd == real_lowcmd_plain);
std::vector<uint8_t> encrypted_lowcmd(real_lowcmd_plain.size());
codec.Encrypt(real_lowcmd_plain.data(), encrypted_lowcmd.data(), real_lowcmd_plain.size());
CHECK(encrypted_lowcmd == real_lowcmd);
const auto state_packet = ReadFile(GO1_PRO_TEST_CAPTURE);
CHECK(state_packet.size() == go1_pro_internal::kLowStateDatagramSize);
LowState state{};
CHECK(codec.DecodeLowState(state_packet.data(), state_packet.size(), state));
CHECK(state.head[0] == 0xfe && state.head[1] == 0xef);
CHECK(state.levelFlag == LOWLEVEL);
CHECK(state.motorState[FR_1].temperature == 50);
CHECK(std::isfinite(state.motorState[FR_1].q));
CHECK(state.wirelessRemote[0] == 0x55 && state.wirelessRemote[1] == 0x51);
const auto old_state_packet = ReadFile(GO1_PRO_TEST_CAPTURE_OLD);
CHECK(old_state_packet.size() == go1_pro_internal::kLowStateDatagramSize);
LowState old_state{};
CHECK(codec.DecodeLowState(old_state_packet.data(), old_state_packet.size(), old_state));
CHECK(old_state.bms.SOC == 76);
CHECK(old_state.motorState[FR_0].temperature == 52);
#endif
Safety safety(LeggedType::Go1);
cmd.motorCmd[FR_0].q = 4.0f;
safety.PositionLimit(cmd);
CHECK(cmd.motorCmd[FR_0].q == 0.78f);
cmd.motorCmd[FR_1].q = static_cast<float>(PosStopF);
safety.PositionLimit(cmd);
CHECK(cmd.motorCmd[FR_1].q == static_cast<float>(PosStopF));
std::atomic<int> callbacks{0};
LoopFunc loop("compat-test", 0.001f, Callback([&callbacks] { ++callbacks; }));
loop.start();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
loop.shutdown();
CHECK(callbacks.load() > 0);
return 0;
}

View File

@@ -0,0 +1,108 @@
import socket
import struct
import tarfile
from pathlib import Path
import pytest
from go1_pro_sdk import Blowfish, parse_low_state
ROOT = Path(__file__).resolve().parents[1]
CAPTURES = ROOT / "data/captures"
STATE_FILE = ROOT / "go1_pro_sdk/_data/blowfish_state.bin"
if not CAPTURES.is_dir():
pytest.skip("private data/captures fixtures are not available", allow_module_level=True)
@pytest.fixture(scope="module")
def blowfish():
return Blowfish.from_state_file(str(STATE_FILE))
def _pcap_udp_packets(path):
packets = []
with path.open("rb") as stream:
header = stream.read(24)
assert len(header) == 24
magic = header[:4]
assert magic in (b"\xd4\xc3\xb2\xa1", b"\xa1\xb2\xc3\xd4")
endian = "<" if magic == b"\xd4\xc3\xb2\xa1" else ">"
linktype = struct.unpack(endian + "I", header[20:24])[0]
assert linktype == 113 # Linux cooked capture v1
while True:
packet_header = stream.read(16)
if not packet_header:
break
assert len(packet_header) == 16
_, _, captured_length, _ = struct.unpack(endian + "IIII", packet_header)
frame = stream.read(captured_length)
assert len(frame) == captured_length
assert struct.unpack(">H", frame[14:16])[0] == 0x0800
ip_offset = 16
ip_header_length = (frame[ip_offset] & 0x0F) * 4
assert frame[ip_offset + 9] == 17
udp_offset = ip_offset + ip_header_length
source_port, target_port, udp_length, _ = struct.unpack(
">HHHH", frame[udp_offset:udp_offset + 8]
)
payload = frame[udp_offset + 8:udp_offset + udp_length]
packets.append((
socket.inet_ntoa(frame[ip_offset + 12:ip_offset + 16]),
socket.inet_ntoa(frame[ip_offset + 16:ip_offset + 20]),
source_port,
target_port,
payload,
))
return packets
def test_real_lowcmd_full_cipher_plain_pair(blowfish):
cipher = (CAPTURES / "real_lowcmd.bin").read_bytes()
plain = (CAPTURES / "real_lowcmd_decrypted.bin").read_bytes()
assert len(cipher) == len(plain) == 616
assert blowfish.decrypt_ecb(cipher) == plain
assert blowfish.encrypt_ecb(plain) == cipher
assert plain[:22] == bytes.fromhex(
"feefff00000000000000000000000000000000003ac0"
)
assert plain[22] == 0x0A # This fixture contains active servo commands.
def test_real_lowcmd_pcap_matches_extracted_first_frame():
packets = _pcap_udp_packets(CAPTURES / "real_lowcmd.pcap")
assert len(packets) == 1406
assert {(src, dst, sport, dport) for src, dst, sport, dport, _ in packets} == {
("192.168.123.161", "192.168.123.10", 8008, 8007)
}
assert {len(payload) for *_, payload in packets} == {616}
assert packets[0][-1] == (CAPTURES / "real_lowcmd.bin").read_bytes()
@pytest.mark.parametrize(
"filename,soc,temperatures",
[
("mcu_response.bin", 76, [52, 42, 40]),
("mcu_response_new.bin", 19, [79, 50, 51]),
],
)
def test_real_lowstate_samples(blowfish, filename, soc, temperatures):
cipher = (CAPTURES / filename).read_bytes()
assert len(cipher) == 858
plain = blowfish.decrypt_ecb(cipher[:856])
assert plain[:4] == b"\xfe\xef\xff\x00"
state = parse_low_state(plain)
assert state.bms.SOC == soc
assert [motor.temperature for motor in state.motorState[:3]] == temperatures
assert state.wirelessRemote[:2] == b"\x55\x51"
def test_memory_dump_reproduces_packaged_blowfish_state():
with tarfile.open(CAPTURES / "blowfish_dump.tar.gz", "r:gz") as archive:
member = archive.extractfile("./rw_7f86a1f000.bin")
assert member is not None
memory = member.read()
extracted = memory[0x232190:0x232190 + 4168]
assert extracted == STATE_FILE.read_bytes()

View File

@@ -61,8 +61,8 @@ def test_plain_crc_at_612():
assert plain[612:616] == expected_crc
def test_encrypted_damping_matches_real(bf):
"""全 damping 加密后, 前 16B 应跟真实 Legged_sport 抓包一致."""
def test_encrypted_default_header_matches_real_capture(bf):
"""默认命令与活动实机命令的公共头部加密结果必须一致."""
cmd = LowCmd()
enc = build_low_cmd_encrypted(cmd, bf)
assert len(enc) == 616

97
tests/test_mcu_client.py Normal file
View File

@@ -0,0 +1,97 @@
from pathlib import Path
from hashlib import sha256
import pytest
from go1_pro_sdk import LowCmd, MCUClient
from go1_pro_sdk.connection import mcu_client as mcu_module
ROOT = Path(__file__).resolve().parents[1]
CAPTURE_FILE = ROOT / "data/captures/mcu_response_new.bin"
if not CAPTURE_FILE.is_file():
pytest.skip("private LowState capture is not available", allow_module_level=True)
CAPTURE = CAPTURE_FILE.read_bytes()
DAMPING_SHA256 = "ca911b04a09d8e8b069c96ac31e410b683fcf1bff625dd203f9efde215888400"
class FakeSocket:
def __init__(self):
self.sent = []
self.recv_batches = []
self.closed = False
self.bound = None
self.blocking = None
def setblocking(self, value):
self.blocking = value
def setsockopt(self, *args):
pass
def bind(self, address):
self.bound = address
def getsockname(self):
return ("0.0.0.0", 45678)
def sendto(self, data, address):
self.sent.append((bytes(data), address))
return len(data)
def recvfrom(self, size):
if not self.recv_batches:
raise BlockingIOError
batch = self.recv_batches[0]
if not batch:
self.recv_batches.pop(0)
raise BlockingIOError
return batch.pop(0)[:size], ("192.168.123.10", 8007)
def close(self):
self.closed = True
@pytest.fixture
def fake_socket(monkeypatch):
sock = FakeSocket()
monkeypatch.setattr(mcu_module.socket, "socket", lambda *args, **kwargs: sock)
monkeypatch.setattr(mcu_module.time, "sleep", lambda duration: None)
return sock
def test_native_client_send_receive_and_close(fake_socket):
with MCUClient() as client:
assert client.local_port == 45678
assert client.send(LowCmd().all_damping()) == 616
packet, address = fake_socket.sent[-1]
assert address == ("192.168.123.10", 8007)
assert sha256(packet).hexdigest() == DAMPING_SHA256
fake_socket.recv_batches = [[CAPTURE]]
state = client.recv_latest()
assert state is client.last_state
assert [motor.temperature for motor in state.motorState[:3]] == [79, 50, 51]
assert state.wirelessRemote[:2] == b"\x55\x51"
assert fake_socket.closed
assert client.sock is None
def test_native_client_ignores_invalid_state(fake_socket):
client = MCUClient()
fake_socket.recv_batches = [[b"\x00" * 858]]
assert client.recv_latest() is None
assert client.last_state is None
def test_native_wake_and_safe_stop(fake_socket):
client = MCUClient()
fake_socket.recv_batches = [[CAPTURE], [CAPTURE], [CAPTURE]]
assert client.wake_mcu(n_frames=3, dt=0) == 3
assert len(fake_socket.sent) == 3
assert all(sha256(packet).hexdigest() == DAMPING_SHA256 for packet, _ in fake_socket.sent)
client.safe_stop(n_frames=2, dt=0)
assert len(fake_socket.sent) == 5
assert all(sha256(packet).hexdigest() == DAMPING_SHA256 for packet, _ in fake_socket.sent)

View File

@@ -1,11 +1,23 @@
"""Contract tests for the official ``robot_interface`` compatibility API."""
import ast
from copy import deepcopy
import inspect
from pathlib import Path
import pytest
import robot_interface as sdk
from go1_pro_sdk import LowState as NativeLowState
from go1_pro_sdk import build_low_cmd_plain
from examples.example_official_compatible_position import initialize_transport
HIGHLEVEL = 0xee
LOWLEVEL = 0xff
POS_STOP_F = 2.146e9
FR_0 = 0
FR_1 = 1
class _FakeMCUClient:
@@ -15,8 +27,8 @@ class _FakeMCUClient:
self.mcu_ip = mcu_ip
self.mcu_port = mcu_port
self.local_port = local_port or 49152
self.next_state = sdk.LowState()
self.next_state.motorState[sdk.FR_1].q = 1.23
self.next_state = NativeLowState()
self.next_state.motorState[FR_1].q = 1.23
self.sent = []
self.closed = False
self.__class__.instances.append(self)
@@ -46,60 +58,117 @@ def fake_client(monkeypatch):
def test_official_names_and_data_types_are_available():
assert sdk.LOWLEVEL == 0xff
assert sdk.LeggedType.Go1 == 2
assert sdk.LeggedType.Go1 != 2
assert int(sdk.LeggedType.Go1) == 2
assert sdk.Go1 is sdk.LeggedType.Go1
assert sdk.FR_0 == 0
assert sdk.RL_2 == 11
assert sdk.BmsState().SOC == 0
assert sdk.HighCmd().velocity == [0.0, 0.0]
assert not hasattr(sdk, "LOWLEVEL")
assert not hasattr(sdk, "FR_0")
assert not hasattr(sdk, "__all__")
assert {name for name in dir(sdk) if not name.startswith("_")} == {
"LeggedType", "Aliengo", "A1", "Go1", "B1",
"RecvEnum", "nonBlock", "block", "blockTimeout",
"UDP", "Safety", "BmsCmd", "BmsState", "Cartesian", "IMU",
"LED", "MotorState", "MotorCmd", "LowState", "LowCmd",
"HighState", "HighCmd", "UDPState",
}
with pytest.raises(TypeError):
sdk.Safety(2)
@pytest.mark.parametrize(
("factory", "kwargs"),
[
(sdk.MotorCmd, {"q": 1.0}),
(sdk.LowCmd, {"levelFlag": LOWLEVEL}),
(sdk.Cartesian, {"x": 1.0}),
(sdk.HighCmd, {"mode": 2}),
],
)
def test_official_struct_constructors_are_no_arg_only(factory, kwargs):
with pytest.raises(TypeError):
factory(**kwargs)
def test_init_cmd_data_uses_official_stop_sentinels():
udp = sdk.UDP(sdk.LOWLEVEL, 0, "192.168.123.10", 8007)
udp = sdk.UDP(LOWLEVEL, 0, "192.168.123.10", 8007)
cmd = sdk.LowCmd()
assert udp.InitCmdData(cmd) is None
assert cmd.levelFlag == sdk.LOWLEVEL
assert cmd.levelFlag == LOWLEVEL
assert len(cmd.motorCmd) == 20
assert cmd.head == [0xfe, 0xef]
assert cmd.SN == [0, 0]
assert cmd.version == [0, 0]
assert isinstance(cmd.motorCmd[0], sdk.MotorCmd)
assert isinstance(cmd.motorCmd[0].mode, int)
assert isinstance(cmd.bms, sdk.BmsCmd)
assert isinstance(cmd.wirelessRemote, list)
assert isinstance(cmd.reserve, int)
assert isinstance(cmd.crc, int)
assert not hasattr(cmd, "set_motor")
assert not hasattr(cmd, "all_damping")
assert not hasattr(cmd.motorCmd[0], "to_bytes")
assert all(m.mode == 0x0A for m in cmd.motorCmd)
assert all(m.q == sdk.PosStopF for m in cmd.motorCmd)
assert all(m.dq == sdk.VelStopF for m in cmd.motorCmd)
assert all(m.q == POS_STOP_F for m in cmd.motorCmd)
assert all(m.dq == 16000.0 for m in cmd.motorCmd)
plain = build_low_cmd_plain(cmd)
assert len(plain) == 616
assert plain[:4] == b"\xfe\xef\xff\x00"
def test_official_lowlevel_send_receive_sequence(fake_client):
udp = sdk.UDP(sdk.LOWLEVEL, 0, "192.168.123.10", 8007)
udp = sdk.UDP(LOWLEVEL, 0, "192.168.123.10", 8007)
cmd = sdk.LowCmd()
state = sdk.LowState()
udp.InitCmdData(cmd)
assert udp.Recv() == 858
assert udp.GetRecv(state) is None
assert state.motorState[sdk.FR_1].q == pytest.approx(1.23)
assert state.motorState[FR_1].q == pytest.approx(1.23)
assert isinstance(state.head, list)
assert isinstance(state.SN, list)
assert isinstance(state.imu.rpy, list)
assert isinstance(state.imu, sdk.IMU)
assert isinstance(state.motorState[0], sdk.MotorState)
assert isinstance(state.bms, sdk.BmsState)
assert not hasattr(state, "remote")
assert not hasattr(state.motorState[0], "estimated_current")
assert not hasattr(state.bms, "voltage_v")
assert isinstance(state.reserve, int)
assert isinstance(state.crc, int)
cmd.motorCmd[sdk.FR_1].q = 1.2
cmd.motorCmd[sdk.FR_1].dq = 0.0
cmd.motorCmd[sdk.FR_1].Kp = 5.0
cmd.motorCmd[sdk.FR_1].Kd = 1.0
cmd.motorCmd[FR_1].q = 1.2
cmd.motorCmd[FR_1].dq = 0.0
cmd.motorCmd[FR_1].Kp = 5.0
cmd.motorCmd[FR_1].Kd = 1.0
assert udp.SetSend(cmd) == 0
assert udp.Send() == 616
client = fake_client.instances[0]
assert client.sent[0].motorCmd[sdk.FR_1].q == pytest.approx(1.2)
assert udp.udpState.SendCount == 1
assert udp.udpState.RecvCount == 1
assert client.sent[0].motorCmd[FR_1].q == pytest.approx(1.2)
assert not hasattr(udp, "udpState")
assert not hasattr(udp, "accessible")
assert not hasattr(udp, "close")
def test_position_limit_skips_stop_sentinel():
cmd = sdk.LowCmd()
sdk.UDP(sdk.LOWLEVEL, 0, "192.168.123.10", 8007).InitCmdData(cmd)
cmd.motorCmd[sdk.FR_1].q = 99.0
sdk.UDP(LOWLEVEL, 0, "192.168.123.10", 8007).InitCmdData(cmd)
cmd.motorCmd[FR_1].q = 99.0
safe = sdk.Safety(sdk.LeggedType.Go1)
assert safe.PositionLimit(cmd) is None
assert cmd.motorCmd[sdk.FR_0].q == sdk.PosStopF
assert cmd.motorCmd[sdk.FR_1].q == pytest.approx(3.5)
assert cmd.motorCmd[FR_0].q == POS_STOP_F
assert cmd.motorCmd[FR_1].q == pytest.approx(3.5)
def test_position_protect_requires_official_explicit_limit():
safe = sdk.Safety(sdk.LeggedType.Go1)
with pytest.raises(TypeError):
safe.PositionProtect(sdk.LowCmd(), sdk.LowState())
def test_bms_command_is_serialized():
@@ -131,10 +200,133 @@ def test_fixed_size_official_arrays_are_validated():
def test_highlevel_udp_reports_the_unsupported_boundary():
udp = sdk.UDP(sdk.HIGHLEVEL, 8080, "192.168.123.161", 8082)
udp = sdk.UDP(HIGHLEVEL, 8080, "192.168.123.161", 8082)
cmd = sdk.HighCmd()
udp.InitCmdData(cmd)
udp.SetSend(cmd)
with pytest.raises(NotImplementedError, match="HighCmd/HighState UDP"):
udp.Send()
def test_portable_initialization_sends_only_stop_sentinels_before_control():
class RecordingUDP:
def __init__(self):
self.cmd = None
self.sent_q = []
self.recv_count = 0
def SetSend(self, cmd):
self.cmd = cmd
return 0
def Send(self):
self.sent_q.append([motor.q for motor in self.cmd.motorCmd])
return 616
def Recv(self):
self.recv_count += 1
return 858 if self.recv_count >= 2 else 0
def GetRecv(self, state):
return None
udp = RecordingUDP()
cmd = sdk.LowCmd()
state = sdk.LowState()
sdk.UDP(LOWLEVEL, 0, "192.168.123.10", 8007).InitCmdData(cmd)
initialize_transport(udp, cmd, state, frames=2, dt=0)
assert len(udp.sent_q) == 2
assert all(all(q == POS_STOP_F for q in frame) for frame in udp.sent_q)
def test_portable_example_only_uses_official_module_exports():
source_path = (
Path(__file__).parents[1]
/ "examples"
/ "example_official_compatible_position.py"
)
tree = ast.parse(source_path.read_text())
sdk_attributes = {
node.attr
for node in ast.walk(tree)
if isinstance(node, ast.Attribute)
and isinstance(node.value, ast.Name)
and node.value.id == "sdk"
}
assert sdk_attributes <= {
"UDP", "Safety", "LeggedType", "LowCmd", "LowState",
}
called_attributes = {
node.func.attr
for node in ast.walk(tree)
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
}
assert called_attributes.isdisjoint({
"set_motor", "all_damping", "wake_mcu", "safe_stop", "close",
})
def test_official_struct_field_snapshot():
expected = {
"BmsCmd": {"off", "reserve"},
"BmsState": {
"version_h", "version_l", "bms_status", "SOC", "current",
"cycle", "BQ_NTC", "MCU_NTC", "cell_vol",
},
"Cartesian": {"x", "y", "z"},
"IMU": {"quaternion", "gyroscope", "accelerometer", "rpy", "temperature"},
"LED": {"r", "g", "b"},
"MotorState": {
"mode", "q", "dq", "ddq", "tauEst", "q_raw", "dq_raw",
"ddq_raw", "temperature", "reserve",
},
"MotorCmd": {"mode", "q", "dq", "tau", "Kp", "Kd", "reserve"},
"LowState": {
"head", "levelFlag", "frameReserve", "SN", "version",
"bandWidth", "imu", "motorState", "bms", "footForce",
"footForceEst", "tick", "wirelessRemote", "reserve", "crc",
},
"LowCmd": {
"head", "levelFlag", "frameReserve", "SN", "version",
"bandWidth", "motorCmd", "bms", "wirelessRemote", "reserve", "crc",
},
"HighState": {
"head", "levelFlag", "frameReserve", "SN", "version",
"bandWidth", "imu", "motorState", "bms", "footForce",
"footForceEst", "mode", "progress", "gaitType",
"footRaiseHeight", "position", "bodyHeight", "velocity",
"yawSpeed", "rangeObstacle", "footPosition2Body",
"footSpeed2Body", "wirelessRemote", "reserve", "crc",
},
"HighCmd": {
"head", "levelFlag", "frameReserve", "SN", "version",
"bandWidth", "mode", "gaitType", "speedLevel",
"footRaiseHeight", "bodyHeight", "position", "euler",
"velocity", "yawSpeed", "bms", "led", "wirelessRemote",
"reserve", "crc",
},
"UDPState": {
"TotalCount", "SendCount", "RecvCount", "SendError",
"FlagError", "RecvCRCError", "RecvLoseError",
},
}
for class_name, field_names in expected.items():
factory = getattr(sdk, class_name)
assert str(inspect.signature(factory)) == "()"
instance = factory()
assert not (field_names - set(dir(instance)))
def test_official_method_snapshot():
assert {
"SetIpPort", "SetRecvTimeout", "SetDisconnectTime",
"SetAccessibleTime", "Send", "Recv", "InitCmdData", "SetSend",
"GetRecv",
} <= set(dir(sdk.UDP))
assert str(inspect.signature(sdk.Safety.PositionLimit)) == "(self, lowcmd)"
assert str(inspect.signature(sdk.Safety.PowerProtect)) == "(self, lowcmd, lowstate, factor)"
assert str(inspect.signature(sdk.Safety.PositionProtect)) == "(self, lowcmd, lowstate, limit)"