Compare commits

...

2 Commits

Author SHA1 Message Date
cyy_mac
f8b849397d cpp对齐官方 2026-07-30 15:25:48 +08:00
cyy_mac
dbfcb95566 python部分官方化对齐接口 2026-07-29 21:50:15 +08:00
45 changed files with 3123 additions and 20 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 协议有显著差异,互不兼容。
@@ -16,6 +16,57 @@ Unitree Go1 **PRO** 机型的低层电机控制 Python SDK。完整逆向 PRO
## 快速上手
### 官方 Python SDK 兼容接口
需要复用官方 `unitree_legged_sdk` Python 示例时,可以继续使用原来的模块名和
调用顺序:
```python
import robot_interface as sdk
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)
udp.Recv()
udp.GetRecv(state)
safe.PowerProtect(cmd, state, 1)
udp.SetSend(cmd)
udp.Send()
```
兼容范围和协议差异见 [`docs/OFFICIAL_API_COMPATIBILITY.md`](docs/OFFICIAL_API_COMPATIBILITY.md)。
官方 `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 系统)
```python
@@ -128,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

@@ -0,0 +1,169 @@
# Unitree 官方 SDK 接口兼容说明
对比基准Unitree [`unitree_legged_sdk`](https://github.com/unitreerobotics/unitree_legged_sdk)
`go1` 分支README 版本 `v3.8.6`,提交
`4539a6c10dfbc9781cea6fcb7d51bc6ddc6f71e1`2023-07-11
## 结论
本项目和官方 SDK 使用相近的低层对象模型,但此前不是调用兼容:本项目入口是
`MCUClient.send()/recv_state()`,官方 Python wrapper 入口是
`robot_interface.UDP.SetSend()/Send()/Recv()/GetRecv()`
当前项目已提供 `robot_interface` 兼容模块。官方低层 Python 示例可保留原来的
导入和主要调用流程,同时底层会使用 Go1 PRO 所需的 Blowfish 加密和 616 字节
私有 `LowCmd` 协议。
反过来,要让本项目编写的程序直接在官方 SDK 上复现,应用必须导入
`robot_interface`,并限制在官方 wrapper 真正导出的公共子集内。应用不应直接导入
`go1_pro_sdk`
## 接口矩阵
| 官方接口 | 本项目原接口 | 当前兼容状态 |
|---|---|---|
| `robot_interface.LowCmd/LowState` | 同名类型 | 已对齐命令和常用状态字段名 |
| `MotorCmd/MotorState/IMU` | 同名类型 | 常用字段已对齐 |
| `BmsState` | `BMS` | 名称已对齐PRO 状态区较短,见下文 |
| `BmsCmd`, `Cartesian`, `UDPState` | 原先缺失 | 已补齐 |
| `UDP(LOWLEVEL, ...)` | `MCUClient(...)` | 已适配标准四参数构造 |
| `InitCmdData` | 原先缺失 | 已实现官方 stop sentinel 初始化 |
| `Recv/GetRecv/SetSend/Send` | `recv_latest/send` | 已适配 |
| `Safety.PositionLimit` | `position_limit` | 已适配,跳过 `PosStopF` |
| `Safety.PowerProtect` | `power_protect` | 调用兼容,算法不是官方闭源实现 |
| `Safety.PositionProtect` | `position_protect` | 调用兼容,保护动作更保守 |
| `HighCmd/HighState` | MQTT `Go1/Go1MQTT` | 类型已提供UDP 传输不支持 |
| `Loop/LoopFunc` | 无 | C++ 已提供;官方 Python wrapper 本身未导出 |
| C++ headers/static library | 无 | 低层 C++ 源码兼容;不承诺二进制 ABI |
## 关键协议差异
官方 SDK 的公开结构不能直接描述本项目实测的 Go1 PRO 线协议。PRO 低层通道需要:
- 616 字节 `LowCmd`CRC 位于 612
- Blowfish ECB 加密;
- `bandWidth` 使用实测字节序和值;
- 858 字节加密状态包中取 856 字节块解密,再解析 807 字节有效状态。
因此兼容层只对齐用户代码接口,不替换本项目现有 codec也不承诺二进制包与官方
EDU SDK 相同。
## 官方风格用法
```python
import robot_interface as sdk
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()
udp.InitCmdData(cmd)
udp.Recv()
udp.GetRecv(state)
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`
## 尚未完全对齐的风险
- 官方 `PowerProtect` 实现在静态库中,头文件只公开签名。本项目当前保护逻辑按
关节力矩限幅和实测过载执行,不等价于官方内部的累计功率算法。
- 官方 `BmsState` 是 34 字节,本项目实测的 PRO 状态布局只给 BMS 分配 24 字节;
`cell_vol` 后半部分不能视为完整的官方数据。
- `LowState.tick` 当前没有从 PRO 包中解析,仍为默认值;`footForce`
`footForceEst` 的偏移来自逆向推断,尚未达到官方字段的同等可信度。
- `head/SN/version/reserve/crc` 在本地原生类型中部分使用 `bytes`,官方 wrapper
使用定长整数数组或整数。命令 builder 接受两种常见赋值形态,但读取后的 Python
类型不保证完全相同。
- 本项目 `LowState` 是 PRO 抓包格式的解释,不应按官方 `#pragma pack(1)` 结构大小
直接做内存映射。
- 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

@@ -30,7 +30,7 @@ from .codec.lowcmd_builder import build_low_cmd_plain, build_low_cmd_encrypted
from .codec.lowstate_parser import parse_low_state
from .types import (
MotorCmd, MotorState, MotorMode,
IMU, BMS,
IMU, BMS, BmsCmd, BmsState,
RemoteState, parse_remote, BUTTON_NAMES,
LowState, LowCmd,
)
@@ -39,7 +39,10 @@ from .safety import (
PowerProtectViolation,
)
from .utils import (
MCU_IP, MCU_PORT,
MCU_IP, MCU_PORT, 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,
JOINT_NAMES, JOINT_TYPE, JOINT_LIMITS, JOINT_LIMITS_MEASURED,
TAU_MAX, KT, DAMPING_POSE,
decode_sn, decode_version,
@@ -54,7 +57,7 @@ __all__ = [
'MCUClient',
# 数据结构
'MotorCmd', 'MotorState', 'MotorMode',
'IMU', 'BMS',
'IMU', 'BMS', 'BmsCmd', 'BmsState',
'RemoteState', 'parse_remote', 'BUTTON_NAMES',
'LowState', 'LowCmd',
# 编解码
@@ -65,6 +68,10 @@ __all__ = [
'PowerProtectViolation',
# 常量
'MCU_IP', 'MCU_PORT',
'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',
'JOINT_NAMES', 'JOINT_TYPE', 'JOINT_LIMITS', 'JOINT_LIMITS_MEASURED',
'TAU_MAX', 'KT', 'DAMPING_POSE',
'decode_sn', 'decode_version',

View File

@@ -33,34 +33,67 @@ from ..utils.common import gen_crc
from .blowfish import Blowfish
def _pack_bytes(value, size: int, field_name: str) -> bytes:
packed = bytes(value)
if len(packed) != size:
raise ValueError(f'{field_name} 必须是 {size} 字节, got {len(packed)}')
return packed
def _pack_u32_pair(value, field_name: str) -> bytes:
if isinstance(value, (bytes, bytearray, memoryview)):
packed = bytes(value)
else:
try:
packed = struct.pack('<2I', *(int(item) for item in value))
except (TypeError, ValueError, struct.error) as exc:
raise ValueError(f'{field_name} 必须是 8 字节或 2 个 uint32') from exc
if len(packed) != 8:
raise ValueError(f'{field_name} 必须是 8 字节, got {len(packed)}')
return packed
def _pack_u32(value, field_name: str) -> bytes:
if isinstance(value, int):
return struct.pack('<I', value)
packed = bytes(value)
if len(packed) != 4:
raise ValueError(f'{field_name} 必须是 4 字节, got {len(packed)}')
return packed
def build_low_cmd_plain(lowcmd: LowCmd) -> bytes:
"""构造 PRO 格式 616B 明文 LowCmd."""
cmd = bytearray(616)
# 0..2 head
cmd[0:2] = lowcmd.head
cmd[0:2] = _pack_bytes(lowcmd.head, 2, 'head')
# 2..3 levelFlag
cmd[2] = lowcmd.levelFlag
# 3..4 frameReserve
cmd[3] = lowcmd.frameReserve
# 4..12 SN
cmd[4:12] = lowcmd.SN
cmd[4:12] = _pack_u32_pair(lowcmd.SN, 'SN')
# 12..20 version
cmd[12:20] = lowcmd.version
cmd[12:20] = _pack_u32_pair(lowcmd.version, 'version')
# 20..22 bandWidth BE
cmd[20:22] = struct.pack('>H', lowcmd.bandWidth)
# 22..562 motorCmd[20] × 27B
if len(lowcmd.motorCmd) != 20:
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
# 562..566 bms (默认 0)
# 562..566 bms
cmd[562] = int(lowcmd.bms.off) & 0xff
cmd[563:566] = _pack_bytes(lowcmd.bms.reserve, 3, 'bms.reserve')
# 566..606 wirelessRemote
cmd[566:606] = lowcmd.wirelessRemote
cmd[566:606] = _pack_bytes(lowcmd.wirelessRemote, 40, 'wirelessRemote')
# 606..610 reserve
cmd[606:610] = lowcmd.reserve
cmd[606:610] = _pack_u32(lowcmd.reserve, 'reserve')
# 610..612 固定填充 0x0000
# 612..616 CRC
cmd[612:616] = gen_crc(memoryview(cmd)[:612])

View File

@@ -0,0 +1,4 @@
"""Compatibility APIs for third-party Unitree SDK code."""
from .robot_interface import *
from .robot_interface import __all__

View File

@@ -0,0 +1,531 @@
"""Compatibility surface for Unitree's ``robot_interface`` Python module.
Only Go1 PRO low-level transport is implemented. The public API mirrors the
official v3.8.6 Python wrapper while the wire codec remains the PRO-specific
encrypted protocol implemented by this project.
"""
from copy import deepcopy
from dataclasses import dataclass, field, fields, is_dataclass
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 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, PosStopF, VelStopF,
MCU_IP, MCU_PORT,
)
class _OfficialEnum(Enum):
def __int__(self):
return self.value
def __index__(self):
return self.value
class LeggedType(_OfficialEnum):
Aliengo = 0
A1 = 1
Go1 = 2
B1 = 3
Aliengo = LeggedType.Aliengo
A1 = LeggedType.A1
Go1 = LeggedType.Go1
B1 = LeggedType.B1
class RecvEnum(_OfficialEnum):
nonBlock = 0x00
block = 0x01
blockTimeout = 0x02
nonBlock = RecvEnum.nonBlock
block = RecvEnum.block
blockTimeout = RecvEnum.blockTimeout
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(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(init=False)
class HighState:
head: List[int] = field(default_factory=lambda: [0, 0])
levelFlag: int = 0
frameReserve: int = 0
SN: List[int] = field(default_factory=lambda: [0, 0])
version: List[int] = field(default_factory=lambda: [0, 0])
bandWidth: int = 0
imu: IMU = field(default_factory=IMU)
motorState: List[MotorState] = field(default_factory=lambda: [MotorState() for _ in range(20)])
bms: BmsState = field(default_factory=BmsState)
footForce: List[int] = field(default_factory=lambda: [0] * 4)
footForceEst: List[int] = field(default_factory=lambda: [0] * 4)
mode: int = 0
progress: float = 0.0
gaitType: int = 0
footRaiseHeight: float = 0.0
position: List[float] = field(default_factory=lambda: [0.0] * 3)
bodyHeight: float = 0.0
velocity: List[float] = field(default_factory=lambda: [0.0] * 3)
yawSpeed: float = 0.0
rangeObstacle: List[float] = field(default_factory=lambda: [0.0] * 4)
footPosition2Body: List[Cartesian] = field(default_factory=lambda: [Cartesian() for _ in range(4)])
footSpeed2Body: List[Cartesian] = field(default_factory=lambda: [Cartesian() for _ in range(4)])
wirelessRemote: List[int] = field(default_factory=lambda: [0] * 40)
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(init=False)
class HighCmd:
head: List[int] = field(default_factory=lambda: [0, 0])
levelFlag: int = 0
frameReserve: int = 0
SN: List[int] = field(default_factory=lambda: [0, 0])
version: List[int] = field(default_factory=lambda: [0, 0])
bandWidth: int = 0
mode: int = 0
gaitType: int = 0
speedLevel: int = 0
footRaiseHeight: float = 0.0
bodyHeight: float = 0.0
position: List[float] = field(default_factory=lambda: [0.0] * 2)
euler: List[float] = field(default_factory=lambda: [0.0] * 3)
velocity: List[float] = field(default_factory=lambda: [0.0] * 2)
yawSpeed: float = 0.0
bms: BmsCmd = field(default_factory=BmsCmd)
led: List[LED] = field(default_factory=lambda: [LED() for _ in range(4)])
wirelessRemote: List[int] = field(default_factory=lambda: [0] * 40)
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(init=False)
class UDPState:
TotalCount: int = 0
SendCount: int = 0
RecvCount: int = 0
SendError: int = 0
FlagError: int = 0
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):
names = (item.name for item in fields(target))
else:
names = vars(source).keys()
for name in names:
if hasattr(source, name):
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`.
The standard ``UDP(level, localPort, targetIP, targetPort)`` constructor is
supported for ``LOWLEVEL``. Custom packet-length constructors and the
official high-level UDP protocol are intentionally not emulated.
"""
def __init__(self, *args):
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) == 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) == 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._accessible = False
self._udp_state = UDPState()
self._client: Optional[MCUClient] = None
self._send_cmd: Optional[LowCmd] = None
self._recv_state: Optional[LowState] = None
self._recv_timeout_ms = 1000
self._disconnect_time = None
self._accessible_time = None
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:
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:
raise NotImplementedError("only the official LOWLEVEL constructor is supported")
if self._client is None:
self._client = MCUClient(
mcu_ip=self._target_ip,
mcu_port=self._target_port,
local_port=self._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._target_ip = str(targetIP)
self._target_port = int(targetPort)
def SetRecvTimeout(self, time):
self._recv_timeout_ms = max(0, int(time))
def SetDisconnectTime(self, callback_dt, disconnectTime):
self._disconnect_time = (float(callback_dt), float(disconnectTime))
def SetAccessibleTime(self, callback_dt, accessibleTime):
self._accessible_time = (float(callback_dt), float(accessibleTime))
def InitCmdData(self, cmd):
if isinstance(cmd, HighCmd):
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()
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
def SetSend(self, cmd):
if isinstance(cmd, HighCmd):
self._send_cmd = deepcopy(cmd)
return 0
if not isinstance(cmd, LowCmd):
raise TypeError("SetSend expects LowCmd or HighCmd")
self._send_cmd = deepcopy(cmd)
return 0
def Send(self):
if self._send_cmd is None:
raise RuntimeError("call InitCmdData and SetSend before Send")
if isinstance(self._send_cmd, HighCmd):
self._ensure_client()
self._udp_state.TotalCount += 1
try:
sent = self._ensure_client().send(self._send_cmd)
except Exception:
self._udp_state.SendError += 1
raise
self._udp_state.SendCount += 1
return sent
def Recv(self):
client = self._ensure_client()
self._udp_state.TotalCount += 1
if self._recv_type == RecvEnum.nonBlock:
state = client.recv_latest()
else:
timeout = self._recv_timeout_ms / 1000.0
state = client.recv_state(timeout=timeout)
if state is None:
return 0
self._recv_state = state
self._accessible = True
self._udp_state.RecvCount += 1
return 858
def GetRecv(self, state):
if isinstance(state, HighState):
self._ensure_client()
if not isinstance(state, LowState):
raise TypeError("GetRecv expects LowState or HighState")
if self._recv_state is not None:
_copy_low_state(self._recv_state, state)
return None
class Safety:
"""Official method names with conservative local safety behavior."""
def __init__(self, legged_type):
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):
position_limit(lowcmd)
return None
def PowerProtect(self, lowcmd, lowstate, factor):
try:
power_protect(lowcmd, lowstate, factor)
except PowerProtectViolation:
return -1
return 0
def PositionProtect(self, lowcmd, lowstate, limit):
return -1 if position_protect(lowcmd, lowstate, limit) else 0
__all__ = [
"LeggedType", "Aliengo", "A1", "Go1", "B1",
"RecvEnum", "nonBlock", "block", "blockTimeout",
"UDP", "Safety", "BmsCmd", "BmsState", "Cartesian", "IMU", "LED",
"MotorState", "MotorCmd", "LowState", "LowCmd", "HighState", "HighCmd",
"UDPState",
]

View File

@@ -7,7 +7,7 @@
"""
from typing import Optional, TYPE_CHECKING
from ..utils.constants import (
JOINT_LIMITS, JOINT_NAMES, JOINT_TYPE, TAU_MAX, CRITICAL_FACTOR,
JOINT_LIMITS, JOINT_NAMES, JOINT_TYPE, TAU_MAX, CRITICAL_FACTOR, PosStopF,
)
if TYPE_CHECKING:
@@ -26,6 +26,8 @@ def position_limit(lowcmd: 'LowCmd') -> int:
jt = JOINT_TYPE[i]
lo, hi = JOINT_LIMITS[jt]
m = lowcmd.motorCmd[i]
if abs(m.q) >= PosStopF * 0.1:
continue
if m.q < lo:
m.q = lo; clamped += 1
elif m.q > hi:
@@ -78,6 +80,8 @@ def position_protect(lowcmd: 'LowCmd', lstate: 'LowState', limit_rad: float) ->
affected = 0
for i in range(12):
m = lowcmd.motorCmd[i]
if abs(m.q) >= PosStopF * 0.1:
continue
try:
actual_q = lstate.motorState[i].q
except (AttributeError, IndexError):

View File

@@ -1,14 +1,14 @@
"""数据结构 (MotorCmd, MotorState, LowState, LowCmd, IMU, BMS, Remote)."""
from .motor import MotorCmd, MotorState, MotorMode
from .imu import IMU
from .bms import BMS
from .bms import BMS, BmsCmd, BmsState
from .remote import RemoteState, parse_remote, BUTTON_NAMES
from .low_state import LowState
from .low_cmd import LowCmd
__all__ = [
'MotorCmd', 'MotorState', 'MotorMode',
'IMU', 'BMS',
'IMU', 'BMS', 'BmsCmd', 'BmsState',
'RemoteState', 'parse_remote', 'BUTTON_NAMES',
'LowState', 'LowCmd',
]

View File

@@ -1,9 +1,16 @@
"""电池管理系统 (BMS) 数据."""
"""电池管理系统 (BMS) 命令和状态数据."""
import struct
from dataclasses import dataclass, field
from typing import List
@dataclass
class BmsCmd:
"""官方 SDK 同名的电池命令结构."""
off: int = 0
reserve: List[int] = field(default_factory=lambda: [0, 0, 0])
@dataclass
class BMS:
"""BMS 反馈."""
@@ -46,3 +53,7 @@ class BMS:
# cell_vol 10 × uint16 LE
bms.cell_vol = [int.from_bytes(data[14 + i*2:14 + (i+1)*2], 'little') for i in range(10)]
return bms
# 官方 Python wrapper 使用 BmsState保留 BMS 作为项目原有名称。
BmsState = BMS

View File

@@ -1,6 +1,7 @@
"""LowCmd — 主控发给 MCU 的控制命令 (PRO 格式 616B 明文 / Blowfish 加密后 616B)."""
from dataclasses import dataclass, field
from typing import List
from .bms import BmsCmd
from .motor import MotorCmd
@@ -17,8 +18,10 @@ class LowCmd:
version: bytes = b'\x00' * 8 # PRO 不填, 全 0
bandWidth: int = 0x3ac0 # 真包实测值
motorCmd: List[MotorCmd] = field(default_factory=lambda: [MotorCmd() for _ in range(20)])
bms: BmsCmd = field(default_factory=BmsCmd)
wirelessRemote: bytes = b'\x00' * 40
reserve: bytes = b'\x00' * 4
crc: int = 0 # 发送时由 builder 重新计算
def set_motor(self, index_or_name, cmd: MotorCmd):
"""设置某个电机命令.

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

@@ -7,7 +7,10 @@ from .common import (
decode_sn, decode_version,
)
from .constants import (
MCU_IP, MCU_PORT,
MCU_IP, MCU_PORT, 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,
JOINT_NAMES, JOINT_TYPE, JOINT_ATTR,
JOINT_LIMITS, JOINT_LIMITS_MEASURED,
TAU_MAX, KT, CRITICAL_FACTOR,
@@ -23,6 +26,10 @@ __all__ = [
'decode_sn', 'decode_version',
# constants
'MCU_IP', 'MCU_PORT',
'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',
'JOINT_NAMES', 'JOINT_TYPE', 'JOINT_ATTR',
'JOINT_LIMITS', 'JOINT_LIMITS_MEASURED',
'TAU_MAX', 'KT', 'CRITICAL_FACTOR',

View File

@@ -8,6 +8,12 @@ MCU_IP = '192.168.123.10'
MCU_PORT = 8007
"""MCU UDP 端口 (从这里发/收 LowCmd/LowState)"""
HIGHLEVEL = 0xEE
LOWLEVEL = 0xFF
TRIGERLEVEL = 0xF0
PosStopF = 2.146e9
VelStopF = 16000.0
# ===== 关节命名/索引 =====
@@ -18,6 +24,9 @@ JOINT_NAMES = [
'RL_0', 'RL_1', 'RL_2', # 9..11 左后
]
FR_, FL_, RR_, RL_ = range(4)
FR_0, FR_1, FR_2, FL_0, FL_1, FL_2, RR_0, RR_1, RR_2, RL_0, RL_1, RL_2 = range(12)
JOINT_TYPE = {i: ('hip' if i % 3 == 0 else 'thigh' if i % 3 == 1 else 'knee')
for i in range(12)}

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

@@ -28,7 +28,9 @@ Homepage = "https://github.com/your-username/go1_pro_sdk"
Documentation = "https://github.com/your-username/go1_pro_sdk/blob/main/docs/"
[tool.setuptools]
py-modules = ["robot_interface"]
packages = ["go1_pro_sdk",
"go1_pro_sdk.compat",
"go1_pro_sdk.codec",
"go1_pro_sdk.connection",
"go1_pro_sdk.highlevel",

3
robot_interface.py Normal file
View File

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

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

@@ -0,0 +1,332 @@
"""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:
instances = []
def __init__(self, mcu_ip, mcu_port, local_port):
self.mcu_ip = mcu_ip
self.mcu_port = mcu_port
self.local_port = local_port or 49152
self.next_state = NativeLowState()
self.next_state.motorState[FR_1].q = 1.23
self.sent = []
self.closed = False
self.__class__.instances.append(self)
def send(self, cmd):
self.sent.append(deepcopy(cmd))
return 616
def recv_latest(self):
return deepcopy(self.next_state)
def recv_state(self, timeout):
return deepcopy(self.next_state)
def close(self):
self.closed = True
@pytest.fixture
def fake_client(monkeypatch):
_FakeMCUClient.instances.clear()
monkeypatch.setattr(
"go1_pro_sdk.compat.robot_interface.MCUClient",
_FakeMCUClient,
)
return _FakeMCUClient
def test_official_names_and_data_types_are_available():
assert sdk.LeggedType.Go1 != 2
assert int(sdk.LeggedType.Go1) == 2
assert sdk.Go1 is sdk.LeggedType.Go1
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(LOWLEVEL, 0, "192.168.123.10", 8007)
cmd = sdk.LowCmd()
assert udp.InitCmdData(cmd) is None
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 == 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(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[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[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[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(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[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():
cmd = sdk.LowCmd()
cmd.bms.off = 0xA5
cmd.bms.reserve = [1, 2, 3]
plain = build_low_cmd_plain(cmd)
assert plain[562:566] == b"\xa5\x01\x02\x03"
def test_official_reserved_field_shapes_are_serialized():
cmd = sdk.LowCmd()
cmd.SN = [0x04030201, 0x08070605]
cmd.version = [0x01020304, 0x05060708]
cmd.reserve = 0x44332211
plain = build_low_cmd_plain(cmd)
assert plain[4:12] == bytes.fromhex("0102030405060708")
assert plain[12:20] == bytes.fromhex("0403020108070605")
assert plain[606:610] == bytes.fromhex("11223344")
def test_fixed_size_official_arrays_are_validated():
cmd = sdk.LowCmd()
cmd.wirelessRemote = [0] * 39
with pytest.raises(ValueError, match="wirelessRemote"):
build_low_cmd_plain(cmd)
def test_highlevel_udp_reports_the_unsupported_boundary():
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)"