Compare commits
5 Commits
81f14e14ca
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e4f632268 | ||
|
|
bd07331734 | ||
|
|
3008203f82 | ||
|
|
f8b849397d | ||
|
|
dbfcb95566 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,8 @@ __pycache__/
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
build-*/
|
||||
CMakeFiles/
|
||||
dist/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
|
||||
129
CMakeLists.txt
Normal file
129
CMakeLists.txt
Normal file
@@ -0,0 +1,129 @@
|
||||
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)
|
||||
option(GO1_PRO_BUILD_BENCHMARKS "Build C++ codec benchmarks" OFF)
|
||||
|
||||
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()
|
||||
|
||||
if(GO1_PRO_BUILD_BENCHMARKS)
|
||||
add_executable(benchmark_cpp_codec tests/cpp/benchmark_codec.cpp)
|
||||
target_include_directories(benchmark_cpp_codec PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
set(GO1_PRO_BENCHMARK_CAPTURE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/data/captures/mcu_response_new.bin" CACHE FILEPATH
|
||||
"Optional real-robot LowState packet used by the C++ codec benchmark")
|
||||
if(EXISTS "${GO1_PRO_BENCHMARK_CAPTURE}")
|
||||
target_compile_definitions(benchmark_cpp_codec PRIVATE
|
||||
GO1_PRO_BENCHMARK_CAPTURE="${GO1_PRO_BENCHMARK_CAPTURE}")
|
||||
endif()
|
||||
target_link_libraries(benchmark_cpp_codec PRIVATE unitree_legged_sdk)
|
||||
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)
|
||||
202
README.md
202
README.md
@@ -1,6 +1,6 @@
|
||||
# Go1 PRO SDK
|
||||
|
||||
Unitree Go1 **PRO** 机型的低层电机控制 Python SDK。完整逆向 PRO 版的私有协议(Blowfish 加密 + 私有 LowCmd 格式),不依赖官方 C++ SDK,Mac 直连可达 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,77 @@ Unitree Go1 **PRO** 机型的低层电机控制 Python SDK。完整逆向 PRO
|
||||
|
||||
## 快速上手
|
||||
|
||||
### 语言与入口
|
||||
|
||||
| 使用方式 | 源码位置 | 构建入口 | 用户入口 |
|
||||
|---|---|---|---|
|
||||
| Python 原生 PRO API | `go1_pro_sdk/` | `pyproject.toml` | `import go1_pro_sdk` |
|
||||
| Python 官方兼容 API | `robot_interface.py`、`go1_pro_sdk/compat/` | `pyproject.toml` | `import robot_interface as sdk` |
|
||||
| Python 可选原生加速 | `fast_lowcmd_cpp/` | `fast_lowcmd_cpp/setup.py` | `FastLowCmdBuilder` |
|
||||
| C++ 官方兼容 SDK | `include/`、`src/` | `CMakeLists.txt` | `#include <unitree_legged_sdk/unitree_legged_sdk.h>` |
|
||||
|
||||
`fast_lowcmd_cpp` 是供 Python 调用的 CPython 扩展,不是 C++ 用户的公共 SDK。C++ 用户
|
||||
只依赖根目录 CMake 生成的 `unitree_legged_sdk` 库以及 `include/unitree_legged_sdk/`
|
||||
中的公共头文件。两套实现共享同一份 PRO 协议和 Blowfish state,但运行时不互相依赖。
|
||||
|
||||
Python 开发环境安装:
|
||||
|
||||
```bash
|
||||
conda activate free_dog_sdk
|
||||
python -m pip install -e .
|
||||
```
|
||||
|
||||
### 官方 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
|
||||
@@ -54,6 +125,96 @@ with MCUClient() as client:
|
||||
client.safe_stop() # 退出前发 damping
|
||||
```
|
||||
|
||||
## 调试与验证
|
||||
|
||||
### 离线完整回归
|
||||
|
||||
以下测试使用 fake socket 和 `data/captures/` 中的实机历史抓包,不连接机器狗,也不会向
|
||||
MCU 发送 UDP 数据。先在仓库根目录准备 Python 包和可选原生扩展:
|
||||
|
||||
```bash
|
||||
conda run -n free_dog_sdk python -m pip install -e .
|
||||
cd fast_lowcmd_cpp
|
||||
PYTHONPATH=.. conda run -n free_dog_sdk python setup.py build_ext --inplace
|
||||
cd ..
|
||||
conda run -n free_dog_sdk python -m pytest -q tests fast_lowcmd_cpp/test_fast_lowcmd.py
|
||||
```
|
||||
|
||||
C++ Release 回归会检查官方结构布局、PRO 明密文字节一致性、LowState 实机抓包、安全接口、
|
||||
Loop,以及安装后被独立 CMake 项目消费:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build-cpp -DCMAKE_BUILD_TYPE=Release \
|
||||
-DGO1_PRO_CAPTURE_DIR="$PWD/data/captures"
|
||||
cmake --build build-cpp --parallel
|
||||
ctest --test-dir build-cpp --output-on-failure
|
||||
```
|
||||
|
||||
配置阶段若显示 `Private data/captures fixtures not found`,说明只运行公共契约测试,未运行
|
||||
实机抓包字节回归。`ctest` 不会运行 `examples/` 中会发送命令的示例程序。
|
||||
|
||||
### 定点调试
|
||||
|
||||
Python 可用 pytest 节点路径只运行单项并显示输出;C++ 可直接运行兼容测试,或交给
|
||||
LLDB(macOS)/GDB(Linux):
|
||||
|
||||
```bash
|
||||
conda run -n free_dog_sdk python -m pytest -vv -s \
|
||||
tests/test_capture_fixtures.py
|
||||
ctest --test-dir build-cpp -R cpp_official_compat -V
|
||||
lldb -- build-cpp/test_cpp_compat
|
||||
# Linux: gdb --args build-cpp/test_cpp_compat
|
||||
```
|
||||
|
||||
需要验证另一份 Blowfish state 或外部抓包目录时:
|
||||
|
||||
```bash
|
||||
GO1_PRO_BLOWFISH_STATE=/path/to/blowfish_state.bin ./build-cpp/test_cpp_compat
|
||||
cmake -S . -B build-cpp -DGO1_PRO_CAPTURE_DIR=/path/to/captures
|
||||
cmake --build build-cpp --target test_cpp_compat --parallel
|
||||
```
|
||||
|
||||
### C++ 编译与运行时检查
|
||||
|
||||
先用严格警告检查可移植性;ASan/UBSan 用于检查越界、生命周期和未定义行为,TSan 单独
|
||||
构建,不能与 ASan 混用:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build-cpp-warn -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_CXX_FLAGS="-Wall -Wextra -Wpedantic"
|
||||
cmake --build build-cpp-warn --parallel
|
||||
ctest --test-dir build-cpp-warn --output-on-failure
|
||||
|
||||
cmake -S . -B build-cpp-sanitize -DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"
|
||||
cmake --build build-cpp-sanitize --parallel
|
||||
ctest --test-dir build-cpp-sanitize --output-on-failure
|
||||
|
||||
cmake -S . -B build-cpp-tsan -DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_CXX_FLAGS="-fsanitize=thread -fno-omit-frame-pointer"
|
||||
cmake --build build-cpp-tsan --parallel
|
||||
ctest --test-dir build-cpp-tsan --output-on-failure
|
||||
```
|
||||
|
||||
### C++ 编解码基准
|
||||
|
||||
基准默认不参与构建。存在 `data/captures/mcu_response_new.bin` 时会同时测量
|
||||
`EncodeLowCmd` 和 `DecodeLowState`;缺少抓包时只测编码:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build-cpp-benchmark -DCMAKE_BUILD_TYPE=Release \
|
||||
-DGO1_PRO_BUILD_BENCHMARKS=ON
|
||||
cmake --build build-cpp-benchmark --target benchmark_cpp_codec --parallel
|
||||
./build-cpp-benchmark/benchmark_cpp_codec
|
||||
```
|
||||
|
||||
### 实机烟雾验证
|
||||
|
||||
自动测试通过后再进入下一节的实机流程。`monitor_state.py` 和 `monitor_remote.py` 虽不发送
|
||||
运动目标,但会执行 `wake_mcu()` 并持续发送 damping LowCmd 以维持回包,因此不是纯被动
|
||||
抓包;运行前仍须悬空机器狗、停掉抢占源,结束后恢复 sportMode。C++ position 示例会发送
|
||||
实际关节位置命令,只应按 [`docs/SAFETY.md`](docs/SAFETY.md) 的检查清单手动运行。
|
||||
|
||||
## 准备工作
|
||||
|
||||
### 1. 提取 Blowfish 密钥(仅需一次)
|
||||
@@ -90,10 +251,10 @@ bash tools/stop_sportmode.sh stop
|
||||
### 3. 跑示例
|
||||
|
||||
```bash
|
||||
# 只读监听 LowState
|
||||
# 状态监听(会发送 damping LowCmd,不是纯被动监听)
|
||||
python examples/monitor_state.py --duration 30 --verbose
|
||||
|
||||
# 监听遥控器
|
||||
# 遥控器监听(会发送 damping LowCmd)
|
||||
python examples/monitor_remote.py
|
||||
|
||||
# 单腿 sin 摆动 (狗悬空, 振幅 0.3 rad)
|
||||
@@ -121,15 +282,36 @@ bash tools/stop_sportmode.sh start
|
||||
## 包结构
|
||||
|
||||
```
|
||||
go1_pro_sdk/
|
||||
├── connection/ # MCUClient: 高层 UDP 客户端
|
||||
├── codec/ # Blowfish + LowCmd 序列化 + LowState 解析
|
||||
├── types/ # MotorCmd, LowState, IMU, BMS, RemoteState 等
|
||||
├── safety/ # PositionLimit / PowerProtect / PositionProtect
|
||||
├── utils/ # CRC, 浮点编解码, 关节常量
|
||||
└── _data/ # blowfish_state.bin
|
||||
go1_pro_sdk/ # Python 原生 PRO SDK
|
||||
├── connection/ # MCUClient: UDP 客户端
|
||||
├── codec/ # Python Blowfish、LowCmd、LowState
|
||||
├── compat/ # 官方 robot_interface Python facade
|
||||
├── highlevel/ # sportMode MQTT API
|
||||
├── safety/ # Python Safety 实现
|
||||
├── types/ # Python 数据结构
|
||||
├── utils/ # CRC、常量和字段编解码
|
||||
└── _data/ # Python wheel 内的 Blowfish state
|
||||
|
||||
robot_interface.py # 官方 Python SDK 同名顶层入口
|
||||
pyproject.toml # Python 构建和安装入口
|
||||
fast_lowcmd_cpp/ # Python 可选 CPython 加速扩展
|
||||
|
||||
include/unitree_legged_sdk/ # C++ 公共头文件,路径与官方一致
|
||||
src/ # C++ 私有实现,不作为公共头文件安装
|
||||
cmake/ # C++ find_package 配置模板
|
||||
CMakeLists.txt # C++ 构建、测试和安装入口
|
||||
|
||||
examples/*.py # Python 示例
|
||||
examples/cpp/ # C++ 同源码兼容示例
|
||||
tests/*.py # Python、实机抓包和 facade 回归
|
||||
tests/cpp/ # C++ 契约、安装消费测试与可选性能基准
|
||||
data/captures/ # 本机实机抓包,存在时自动参与回归
|
||||
```
|
||||
|
||||
Python 和 C++ 的公共 API 不交叉包含:Python 安装由 `pyproject.toml` 管理,不会安装
|
||||
C++ 头文件;C++ 安装由 CMake 管理,不会安装 Python 包。两边唯一共享的运行数据是
|
||||
`blowfish_state.bin`,安装时分别进入 Python package data 和 C++ data directory。
|
||||
|
||||
## 文档
|
||||
|
||||
- `docs/PROTOCOL.md` — PRO LowCmd/LowState 字节级格式规格
|
||||
|
||||
7
cmake/unitree_legged_sdkConfig.cmake.in
Normal file
7
cmake/unitree_legged_sdkConfig.cmake.in
Normal 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)
|
||||
@@ -1,6 +1,23 @@
|
||||
# 包结构与数据流
|
||||
|
||||
## 模块依赖图
|
||||
## 仓库分层
|
||||
|
||||
本仓库同时提供 Python SDK 和独立 C++ SDK,两者按构建系统和公共入口区分:
|
||||
|
||||
| 层 | Python | C++ |
|
||||
|---|---|---|
|
||||
| 公共 API | `go1_pro_sdk/`、`robot_interface.py` | `include/unitree_legged_sdk/` |
|
||||
| 私有实现 | Python package 子模块 | `src/` |
|
||||
| 构建入口 | `pyproject.toml` | `CMakeLists.txt` |
|
||||
| 示例 | `examples/*.py` | `examples/cpp/*.cpp` |
|
||||
| 测试 | `tests/*.py` | `tests/cpp/` |
|
||||
|
||||
`fast_lowcmd_cpp/` 属于 Python 侧:它生成 CPython extension,用来加速 Python 的命令
|
||||
编解码和状态解析。它不提供 C++ 公共头文件,也不应被 C++ 应用直接链接。
|
||||
|
||||
## Python 架构
|
||||
|
||||
### 模块依赖图
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
@@ -26,7 +43,7 @@
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## 数据流: 收一帧 → 用户处理 → 发命令
|
||||
### 数据流: 收一帧 → 用户处理 → 发命令
|
||||
|
||||
```
|
||||
UDP 858B 密文 UDP 616B 密文
|
||||
@@ -64,16 +81,16 @@
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 各包职责
|
||||
### 各包职责
|
||||
|
||||
### utils/
|
||||
#### utils/
|
||||
|
||||
最底层。纯函数, 无副作用, 无依赖其他模块。
|
||||
|
||||
- `common.py`: CRC, float↔hex, tau↔2B, Kp↔2B, Kd↔2B, decode_sn/version
|
||||
- `constants.py`: MCU 地址, 关节命名, 限位, TAU_MAX, DAMPING_POSE
|
||||
|
||||
### types/
|
||||
#### types/
|
||||
|
||||
数据结构 (dataclass)。依赖 utils, 不依赖 codec/safety/connection。
|
||||
|
||||
@@ -84,7 +101,7 @@
|
||||
- `low_cmd.py`: LowCmd (12 motorCmd + 元数据)
|
||||
- `low_state.py`: LowState (12 motorState + IMU + BMS + remote 等)
|
||||
|
||||
### codec/
|
||||
#### codec/
|
||||
|
||||
加解密和序列化。依赖 types + utils。
|
||||
|
||||
@@ -92,21 +109,21 @@
|
||||
- `lowcmd_builder.py`: LowCmd → 616B 明文 → 加密后 616B
|
||||
- `lowstate_parser.py`: 解密后 807B → LowState 结构
|
||||
|
||||
### safety/
|
||||
#### safety/
|
||||
|
||||
控制保护层。依赖 types + utils。
|
||||
|
||||
- `safety.py`: PositionLimit/PowerProtect/PositionProtect 三个保护 + 统一入口 apply_safety
|
||||
|
||||
### connection/
|
||||
#### connection/
|
||||
|
||||
UDP 高层抽象。依赖前面所有。
|
||||
|
||||
- `mcu_client.py`: MCUClient (socket + Blowfish + 序列化 + safe_stop)
|
||||
|
||||
## 用户三种用法
|
||||
### 用户三种用法
|
||||
|
||||
### Level 1: 高层 (推荐)
|
||||
#### Level 1: 高层 (推荐)
|
||||
|
||||
```python
|
||||
from go1_pro_sdk import MCUClient, LowCmd, MotorCmd, MotorMode
|
||||
@@ -120,7 +137,7 @@ with MCUClient() as client:
|
||||
client.safe_stop()
|
||||
```
|
||||
|
||||
### Level 2: 中层 (自己管 socket, 用编解码)
|
||||
#### Level 2: 中层 (自己管 socket, 用编解码)
|
||||
|
||||
```python
|
||||
from go1_pro_sdk import Blowfish, build_low_cmd_encrypted, parse_low_state, LowCmd
|
||||
@@ -136,7 +153,49 @@ data, _ = sock.recvfrom(2048)
|
||||
state = parse_low_state(bf.decrypt_ecb(data[:856]))
|
||||
```
|
||||
|
||||
### Level 3: 底层 (诊断/逆向)
|
||||
#### Level 3: 底层 (诊断/逆向)
|
||||
|
||||
直接用 `Blowfish.encrypt_block(b8)` / `Blowfish.decrypt_block(b8)`, 自己处理 8B 块。
|
||||
适合协议研究或字节级 diff。
|
||||
|
||||
## C++ 架构
|
||||
|
||||
```text
|
||||
应用源码
|
||||
#include <unitree_legged_sdk/unitree_legged_sdk.h>
|
||||
│
|
||||
▼
|
||||
include/unitree_legged_sdk/ 公共、官方兼容声明
|
||||
comm.h / udp.h / safety.h / loop.h / quadruped.h
|
||||
│
|
||||
▼
|
||||
src/
|
||||
udp.cpp POSIX UDP、线程安全收发、官方方法适配
|
||||
safety.cpp PositionLimit/PowerProtect/PositionProtect
|
||||
loop.cpp Loop/LoopFunc 调度和 Linux CPU affinity
|
||||
pro_codec.cpp 616B LowCmd、858B LowState、CRC、Blowfish
|
||||
quadruped.cpp 版本、长度常量和 InitEnvironment
|
||||
```
|
||||
|
||||
`src/pro_codec.h` 是库内部头文件,不安装给用户。C++ 应用只能包含
|
||||
`include/unitree_legged_sdk/` 下的头文件,这保证业务源码切换到官方 SDK 时不依赖 PRO
|
||||
专有声明。PRO 的加密和私有线协议全部封装在 `UDP` 实现内部。
|
||||
|
||||
CMake 对外提供:
|
||||
|
||||
- 构建树目标:`unitree_legged_sdk`;
|
||||
- namespaced alias:`unitree_legged_sdk::unitree_legged_sdk`;
|
||||
- 安装后的 `find_package(unitree_legged_sdk CONFIG REQUIRED)`;
|
||||
- 安装目录中的公共头文件、静态库、CMake config 和 Blowfish state。
|
||||
|
||||
## 共享边界
|
||||
|
||||
Python 与 C++ 没有运行时语言绑定关系,也不会互相调用。它们共享的是协议规格、实机
|
||||
抓包回归基准和 Blowfish state。修改协议实现时必须同时运行:
|
||||
|
||||
```bash
|
||||
conda run -n free_dog_sdk python -m pytest -q tests fast_lowcmd_cpp/test_fast_lowcmd.py
|
||||
cmake -S . -B build-cpp -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build-cpp
|
||||
ctest --test-dir build-cpp --output-on-failure
|
||||
```
|
||||
|
||||
169
docs/OFFICIAL_API_COMPATIBILITY.md
Normal file
169
docs/OFFICIAL_API_COMPATIBILITY.md
Normal 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。
|
||||
@@ -1,5 +1,12 @@
|
||||
# Examples
|
||||
|
||||
## 目录划分
|
||||
|
||||
- `examples/*.py`:Python 原生 PRO API 和 Python 官方兼容 facade 示例;
|
||||
- `examples/cpp/*.cpp`:独立 C++ SDK 示例,只使用官方 C++ 公共接口。
|
||||
|
||||
`fast_lowcmd_cpp` 是 Python 可选加速扩展,不是另一套 C++ 示例入口。
|
||||
|
||||
## 高层 (HighLevel) — 通过 sportMode + MQTT, 不需停 Pi 进程
|
||||
|
||||
适合: 走/跳/姿态/表演动作. 跟 lowlevel 控制方式互不兼容, 同时只用一种.
|
||||
@@ -89,3 +96,16 @@ python examples/example_remote_control.py
|
||||
## 安全
|
||||
|
||||
所有控制类示例都内置 `apply_safety(cmd, state, power_factor=1)` (10% 力矩限制), 出问题会立即停机. 见 `docs/SAFETY.md`.
|
||||
|
||||
## C++ 官方兼容示例
|
||||
|
||||
[`cpp/example_official_compatible_position.cpp`](cpp/example_official_compatible_position.cpp)
|
||||
使用与官方 `unitree_legged_sdk` 相同的头文件、命名空间、`UDP` 和 `Safety` API。构建:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build-cpp -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build-cpp --target example_official_compatible_position
|
||||
```
|
||||
|
||||
该示例会先发送 stop sentinel,收到第一帧有效 `LowState` 后才启用位置目标。PRO 环境
|
||||
链接本项目库;官方 Go1 环境使用相同源码重新链接官方库。
|
||||
|
||||
60
examples/cpp/example_official_compatible_position.cpp
Normal file
60
examples/cpp/example_official_compatible_position.cpp
Normal 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;
|
||||
}
|
||||
62
examples/example_official_compatible_position.py
Normal file
62
examples/example_official_compatible_position.py
Normal 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()
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Fast MCU client using native LowState decrypt/parse.
|
||||
"""Fast MCU client using native LowState decrypt/parse and UDP drain.
|
||||
|
||||
The public shape mirrors go1_pro_sdk.connection.MCUClient for deployment code,
|
||||
while keeping the receive hot path out of pure Python.
|
||||
"""
|
||||
import socket
|
||||
import time
|
||||
|
||||
from fast_lowcmd import FastLowCmdBuilder, default_state_path
|
||||
from go1_fast_lowcmd import FastNativeMCUClient
|
||||
from go1_pro_sdk import MCU_IP, MCU_PORT
|
||||
from go1_pro_sdk.utils.constants import RCVBUF_SIZE
|
||||
|
||||
@@ -127,16 +127,19 @@ class FastMCUClient:
|
||||
raise ValueError("FastMCUClient currently supports only little-endian Blowfish state")
|
||||
self.mcu_ip = mcu_ip
|
||||
self.mcu_port = mcu_port
|
||||
self.builder = FastLowCmdBuilder(state_path or default_state_path())
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setblocking(False)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, RCVBUF_SIZE)
|
||||
sock.bind(("", local_port))
|
||||
self.sock = sock
|
||||
self.local_port = sock.getsockname()[1]
|
||||
resolved_state_path = state_path or default_state_path()
|
||||
self.builder = FastLowCmdBuilder(resolved_state_path)
|
||||
self._native = FastNativeMCUClient(
|
||||
resolved_state_path,
|
||||
mcu_ip,
|
||||
int(mcu_port),
|
||||
int(local_port),
|
||||
int(RCVBUF_SIZE),
|
||||
endian,
|
||||
)
|
||||
self.local_port = self._native.local_port()
|
||||
self._last_state = None
|
||||
self.backend = "cpp_lowcmd_cpp_lowstate"
|
||||
self.backend = "cpp_lowcmd_cpp_lowstate_cpp_udp"
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
@@ -145,27 +148,16 @@ class FastMCUClient:
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self.sock:
|
||||
self.sock.close()
|
||||
self.sock = None
|
||||
native = getattr(self, "_native", None)
|
||||
if native is not None:
|
||||
native.close()
|
||||
self._native = None
|
||||
|
||||
def send_raw(self, raw_cipher):
|
||||
self.sock.sendto(raw_cipher, (self.mcu_ip, self.mcu_port))
|
||||
return len(raw_cipher)
|
||||
return self._native.send_raw(raw_cipher)
|
||||
|
||||
def recv_latest(self):
|
||||
last_data = None
|
||||
while True:
|
||||
try:
|
||||
data, _ = self.sock.recvfrom(2048)
|
||||
last_data = data
|
||||
except (BlockingIOError, socket.timeout):
|
||||
break
|
||||
|
||||
if last_data is None:
|
||||
return None
|
||||
|
||||
fields = self.builder.decrypt_lowstate(last_data)
|
||||
fields = self._native.recv_latest_fields()
|
||||
if fields is None:
|
||||
return None
|
||||
self._last_state = FastLowState(fields)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#define PY_SSIZE_T_CLEAN
|
||||
#include <Python.h>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
@@ -9,6 +10,12 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Py_ssize_t LOWCMD_SIZE = 616;
|
||||
@@ -460,6 +467,31 @@ struct FastLowCmdBuilder {
|
||||
uint32_t S[4][256];
|
||||
};
|
||||
|
||||
bool load_blowfish_state(const char* state_path, uint32_t P[18], uint32_t S[4][256]) {
|
||||
std::ifstream f(state_path, std::ios::binary);
|
||||
if (!f) {
|
||||
PyErr_Format(PyExc_FileNotFoundError, "cannot open Blowfish state file: %s", state_path);
|
||||
return false;
|
||||
}
|
||||
std::array<uint8_t, 4168> buf{};
|
||||
f.read(reinterpret_cast<char*>(buf.data()), static_cast<std::streamsize>(buf.size()));
|
||||
if (f.gcount() < static_cast<std::streamsize>(buf.size())) {
|
||||
PyErr_Format(PyExc_ValueError, "Blowfish state file must contain at least 4168 bytes: %s", state_path);
|
||||
return false;
|
||||
}
|
||||
const uint8_t* p = buf.data();
|
||||
for (int i = 0; i < 18; ++i) {
|
||||
P[i] = get_u32_le(p + i * 4);
|
||||
}
|
||||
p += 18 * 4;
|
||||
for (int s = 0; s < 4; ++s) {
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
S[s][i] = get_u32_le(p + (s * 256 + i) * 4);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t bf_f(FastLowCmdBuilder* self, uint32_t x) {
|
||||
return (((self->S[0][(x >> 24) & 0xffu] + self->S[1][(x >> 16) & 0xffu]) & 0xffffffffu)
|
||||
^ self->S[2][(x >> 8) & 0xffu]) + self->S[3][x & 0xffu];
|
||||
@@ -623,28 +655,7 @@ int FastLowCmdBuilder_init(FastLowCmdBuilder* self, PyObject* args, PyObject* kw
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::ifstream f(state_path, std::ios::binary);
|
||||
if (!f) {
|
||||
PyErr_Format(PyExc_FileNotFoundError, "cannot open Blowfish state file: %s", state_path);
|
||||
return -1;
|
||||
}
|
||||
std::array<uint8_t, 4168> buf{};
|
||||
f.read(reinterpret_cast<char*>(buf.data()), static_cast<std::streamsize>(buf.size()));
|
||||
if (f.gcount() < static_cast<std::streamsize>(buf.size())) {
|
||||
PyErr_Format(PyExc_ValueError, "Blowfish state file must contain at least 4168 bytes: %s", state_path);
|
||||
return -1;
|
||||
}
|
||||
const uint8_t* p = buf.data();
|
||||
for (int i = 0; i < 18; ++i) {
|
||||
self->P[i] = get_u32_le(p + i * 4);
|
||||
}
|
||||
p += 18 * 4;
|
||||
for (int s = 0; s < 4; ++s) {
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
self->S[s][i] = get_u32_le(p + (s * 256 + i) * 4);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return load_blowfish_state(state_path, self->P, self->S) ? 0 : -1;
|
||||
}
|
||||
|
||||
PyObject* FastLowCmdBuilder_build_plain_damping(FastLowCmdBuilder*, PyObject*) {
|
||||
@@ -865,27 +876,19 @@ PyObject* FastLowCmdBuilder_encrypt(FastLowCmdBuilder* self, PyObject* args) {
|
||||
return result;
|
||||
}
|
||||
|
||||
PyObject* FastLowCmdBuilder_decrypt_lowstate(FastLowCmdBuilder* self, PyObject* args) {
|
||||
Py_buffer view;
|
||||
if (!PyArg_ParseTuple(args, "y*", &view)) {
|
||||
return nullptr;
|
||||
}
|
||||
if (view.len < 807) {
|
||||
PyBuffer_Release(&view);
|
||||
PyObject* parse_lowstate_packet(FastLowCmdBuilder* self, const uint8_t* cipher, Py_ssize_t len) {
|
||||
if (len < 807) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
Py_ssize_t aligned = (view.len / 8) * 8;
|
||||
Py_ssize_t aligned = (len / 8) * 8;
|
||||
if (aligned < 807) {
|
||||
PyBuffer_Release(&view);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
const uint8_t* cipher = reinterpret_cast<const uint8_t*>(view.buf);
|
||||
std::vector<uint8_t> data(static_cast<size_t>(aligned));
|
||||
for (Py_ssize_t i = 0; i < aligned; i += 8) {
|
||||
decrypt_block_le(self, cipher + i, data.data() + i);
|
||||
}
|
||||
PyBuffer_Release(&view);
|
||||
|
||||
if (data[0] != 0xfe || data[1] != 0xef || data[2] != 0xff || data[3] != 0x00) {
|
||||
Py_RETURN_NONE;
|
||||
@@ -913,7 +916,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));
|
||||
}
|
||||
@@ -983,6 +986,207 @@ PyObject* FastLowCmdBuilder_decrypt_lowstate(FastLowCmdBuilder* self, PyObject*
|
||||
return out;
|
||||
}
|
||||
|
||||
PyObject* FastLowCmdBuilder_decrypt_lowstate(FastLowCmdBuilder* self, PyObject* args) {
|
||||
Py_buffer view;
|
||||
if (!PyArg_ParseTuple(args, "y*", &view)) {
|
||||
return nullptr;
|
||||
}
|
||||
PyObject* result = parse_lowstate_packet(
|
||||
self, reinterpret_cast<const uint8_t*>(view.buf), view.len);
|
||||
PyBuffer_Release(&view);
|
||||
return result;
|
||||
}
|
||||
|
||||
struct FastNativeMCUClient {
|
||||
PyObject_HEAD
|
||||
int sock_fd;
|
||||
int local_port;
|
||||
FastLowCmdBuilder codec;
|
||||
};
|
||||
|
||||
void close_native_socket(FastNativeMCUClient* self) {
|
||||
if (self->sock_fd >= 0) {
|
||||
close(self->sock_fd);
|
||||
self->sock_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* FastNativeMCUClient_new(PyTypeObject* type, PyObject*, PyObject*) {
|
||||
auto* self = reinterpret_cast<FastNativeMCUClient*>(type->tp_alloc(type, 0));
|
||||
if (self) {
|
||||
self->sock_fd = -1;
|
||||
self->local_port = 0;
|
||||
std::memset(&self->codec, 0, sizeof(self->codec));
|
||||
}
|
||||
return reinterpret_cast<PyObject*>(self);
|
||||
}
|
||||
|
||||
void FastNativeMCUClient_dealloc(FastNativeMCUClient* self) {
|
||||
close_native_socket(self);
|
||||
Py_TYPE(self)->tp_free(reinterpret_cast<PyObject*>(self));
|
||||
}
|
||||
|
||||
int set_nonblocking(int fd) {
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0) {
|
||||
return -1;
|
||||
}
|
||||
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
}
|
||||
|
||||
int FastNativeMCUClient_init(FastNativeMCUClient* self, PyObject* args, PyObject* kwargs) {
|
||||
const char* state_path = nullptr;
|
||||
const char* mcu_ip = "192.168.123.10";
|
||||
const char* endian = "little";
|
||||
int mcu_port = 8007;
|
||||
int local_port = 0;
|
||||
int rcvbuf_size = 4096;
|
||||
static const char* kwlist[] = {
|
||||
"state_path", "mcu_ip", "mcu_port", "local_port", "rcvbuf_size", "endian", nullptr};
|
||||
if (!PyArg_ParseTupleAndKeywords(
|
||||
args, kwargs, "s|siiis", const_cast<char**>(kwlist),
|
||||
&state_path, &mcu_ip, &mcu_port, &local_port, &rcvbuf_size, &endian)) {
|
||||
return -1;
|
||||
}
|
||||
if (std::strcmp(endian, "little") != 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "FastNativeMCUClient currently supports only little-endian Blowfish state");
|
||||
return -1;
|
||||
}
|
||||
close_native_socket(self);
|
||||
if (!load_blowfish_state(state_path, self->codec.P, self->codec.S)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (self->sock_fd < 0) {
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
return -1;
|
||||
}
|
||||
int reuse = 1;
|
||||
setsockopt(self->sock_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
|
||||
if (rcvbuf_size > 0) {
|
||||
setsockopt(self->sock_fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf_size, sizeof(rcvbuf_size));
|
||||
}
|
||||
if (set_nonblocking(self->sock_fd) < 0) {
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
close_native_socket(self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sockaddr_in local{};
|
||||
local.sin_family = AF_INET;
|
||||
local.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
local.sin_port = htons(static_cast<uint16_t>(local_port));
|
||||
if (bind(self->sock_fd, reinterpret_cast<sockaddr*>(&local), sizeof(local)) < 0) {
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
close_native_socket(self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sockaddr_in target{};
|
||||
target.sin_family = AF_INET;
|
||||
target.sin_port = htons(static_cast<uint16_t>(mcu_port));
|
||||
if (inet_pton(AF_INET, mcu_ip, &target.sin_addr) != 1) {
|
||||
PyErr_Format(PyExc_ValueError, "invalid IPv4 target: %s", mcu_ip);
|
||||
close_native_socket(self);
|
||||
return -1;
|
||||
}
|
||||
if (connect(self->sock_fd, reinterpret_cast<sockaddr*>(&target), sizeof(target)) < 0) {
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
close_native_socket(self);
|
||||
return -1;
|
||||
}
|
||||
|
||||
sockaddr_in actual{};
|
||||
socklen_t actual_len = sizeof(actual);
|
||||
if (getsockname(self->sock_fd, reinterpret_cast<sockaddr*>(&actual), &actual_len) == 0) {
|
||||
self->local_port = ntohs(actual.sin_port);
|
||||
} else {
|
||||
self->local_port = local_port;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* FastNativeMCUClient_close(FastNativeMCUClient* self, PyObject*) {
|
||||
close_native_socket(self);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* FastNativeMCUClient_local_port(FastNativeMCUClient* self, PyObject*) {
|
||||
return PyLong_FromLong(self->local_port);
|
||||
}
|
||||
|
||||
PyObject* FastNativeMCUClient_send_raw(FastNativeMCUClient* self, PyObject* args) {
|
||||
if (self->sock_fd < 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "FastNativeMCUClient is closed");
|
||||
return nullptr;
|
||||
}
|
||||
Py_buffer view;
|
||||
if (!PyArg_ParseTuple(args, "y*", &view)) {
|
||||
return nullptr;
|
||||
}
|
||||
ssize_t sent = 0;
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
sent = send(self->sock_fd, view.buf, static_cast<size_t>(view.len), 0);
|
||||
Py_END_ALLOW_THREADS
|
||||
PyBuffer_Release(&view);
|
||||
if (sent < 0) {
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
return nullptr;
|
||||
}
|
||||
return PyLong_FromSsize_t(sent);
|
||||
}
|
||||
|
||||
PyObject* FastNativeMCUClient_recv_latest_fields(FastNativeMCUClient* self, PyObject*) {
|
||||
if (self->sock_fd < 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "FastNativeMCUClient is closed");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 2048> buffer{};
|
||||
std::array<uint8_t, 2048> latest{};
|
||||
ssize_t latest_len = -1;
|
||||
while (true) {
|
||||
ssize_t received = 0;
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
received = recv(self->sock_fd, buffer.data(), buffer.size(), 0);
|
||||
Py_END_ALLOW_THREADS
|
||||
if (received > 0) {
|
||||
latest_len = received;
|
||||
std::memcpy(latest.data(), buffer.data(), static_cast<size_t>(received));
|
||||
continue;
|
||||
}
|
||||
if (received == 0) {
|
||||
break;
|
||||
}
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
break;
|
||||
}
|
||||
PyErr_SetFromErrno(PyExc_OSError);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (latest_len < 0) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
return parse_lowstate_packet(&self->codec, latest.data(), latest_len);
|
||||
}
|
||||
|
||||
PyMethodDef FastNativeMCUClient_methods[] = {
|
||||
{"close", reinterpret_cast<PyCFunction>(FastNativeMCUClient_close), METH_NOARGS,
|
||||
"Close the native UDP socket."},
|
||||
{"local_port", reinterpret_cast<PyCFunction>(FastNativeMCUClient_local_port), METH_NOARGS,
|
||||
"Return the bound local UDP port."},
|
||||
{"send_raw", reinterpret_cast<PyCFunction>(FastNativeMCUClient_send_raw), METH_VARARGS,
|
||||
"Send an already encrypted LowCmd packet through the native UDP socket."},
|
||||
{"recv_latest_fields", reinterpret_cast<PyCFunction>(FastNativeMCUClient_recv_latest_fields), METH_NOARGS,
|
||||
"Drain pending UDP packets and return parsed fields for the newest LowState packet."},
|
||||
{nullptr, nullptr, 0, nullptr},
|
||||
};
|
||||
|
||||
PyMethodDef FastLowCmdBuilder_methods[] = {
|
||||
{"build_plain_damping", reinterpret_cast<PyCFunction>(FastLowCmdBuilder_build_plain_damping), METH_NOARGS,
|
||||
"Build a plain all-damping LowCmd packet."},
|
||||
@@ -1015,6 +1219,10 @@ PyTypeObject FastLowCmdBuilderType = {
|
||||
PyVarObject_HEAD_INIT(nullptr, 0)
|
||||
};
|
||||
|
||||
PyTypeObject FastNativeMCUClientType = {
|
||||
PyVarObject_HEAD_INIT(nullptr, 0)
|
||||
};
|
||||
|
||||
PyModuleDef module = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
"go1_fast_lowcmd",
|
||||
@@ -1038,6 +1246,19 @@ PyMODINIT_FUNC PyInit_go1_fast_lowcmd(void) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FastNativeMCUClientType.tp_name = "go1_fast_lowcmd.FastNativeMCUClient";
|
||||
FastNativeMCUClientType.tp_basicsize = sizeof(FastNativeMCUClient);
|
||||
FastNativeMCUClientType.tp_flags = Py_TPFLAGS_DEFAULT;
|
||||
FastNativeMCUClientType.tp_doc = "Native UDP MCU client with C++ LowState drain/decrypt/parse.";
|
||||
FastNativeMCUClientType.tp_new = FastNativeMCUClient_new;
|
||||
FastNativeMCUClientType.tp_init = reinterpret_cast<initproc>(FastNativeMCUClient_init);
|
||||
FastNativeMCUClientType.tp_dealloc = reinterpret_cast<destructor>(FastNativeMCUClient_dealloc);
|
||||
FastNativeMCUClientType.tp_methods = FastNativeMCUClient_methods;
|
||||
|
||||
if (PyType_Ready(&FastNativeMCUClientType) < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObject* m = PyModule_Create(&module);
|
||||
if (!m) {
|
||||
return nullptr;
|
||||
@@ -1048,6 +1269,12 @@ PyMODINIT_FUNC PyInit_go1_fast_lowcmd(void) {
|
||||
Py_DECREF(m);
|
||||
return nullptr;
|
||||
}
|
||||
Py_INCREF(&FastNativeMCUClientType);
|
||||
if (PyModule_AddObject(m, "FastNativeMCUClient", reinterpret_cast<PyObject*>(&FastNativeMCUClientType)) < 0) {
|
||||
Py_DECREF(&FastNativeMCUClientType);
|
||||
Py_DECREF(m);
|
||||
return nullptr;
|
||||
}
|
||||
PyModule_AddIntConstant(m, "LOWCMD_SIZE", LOWCMD_SIZE);
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import os
|
||||
import pytest
|
||||
|
||||
from fast_lowcmd import FastLowCmdBuilder, default_state_path
|
||||
from fast_mcu import FastMCUClient
|
||||
from go1_fast_lowcmd import FastNativeMCUClient
|
||||
from go1_pro_sdk import Blowfish, LowCmd, MotorCmd, MotorMode, PowerProtectViolation
|
||||
from go1_pro_sdk import apply_safety
|
||||
from go1_pro_sdk.codec.lowcmd_builder import build_low_cmd_encrypted, build_low_cmd_plain
|
||||
@@ -140,9 +142,30 @@ def test_state_file_is_required():
|
||||
FastLowCmdBuilder(os.path.join(os.path.dirname(__file__), "missing_state.bin"))
|
||||
|
||||
|
||||
def test_native_mcu_client_empty_recv():
|
||||
client = FastNativeMCUClient(default_state_path(), "127.0.0.1", 8007, 0, 4096, "little")
|
||||
try:
|
||||
assert client.local_port() > 0
|
||||
assert client.recv_latest_fields() is None
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def test_fast_mcu_uses_native_udp_client():
|
||||
client = FastMCUClient(mcu_ip="127.0.0.1", mcu_port=8007, local_port=0)
|
||||
try:
|
||||
assert client.backend == "cpp_lowcmd_cpp_lowstate_cpp_udp"
|
||||
assert client.local_port > 0
|
||||
assert client.recv_latest() is None
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
def test_lowstate_decrypt_matches_python(builder, bf):
|
||||
root = os.path.dirname(os.path.dirname(__file__))
|
||||
packet_path = os.path.join(root, "data", "captures", "mcu_response.bin")
|
||||
if not os.path.exists(packet_path):
|
||||
pytest.skip("private LowState capture is not available")
|
||||
with open(packet_path, "rb") as f:
|
||||
packet = f.read()
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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])
|
||||
|
||||
4
go1_pro_sdk/compat/__init__.py
Normal file
4
go1_pro_sdk/compat/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Compatibility APIs for third-party Unitree SDK code."""
|
||||
|
||||
from .robot_interface import *
|
||||
from .robot_interface import __all__
|
||||
531
go1_pro_sdk/compat/robot_interface.py
Normal file
531
go1_pro_sdk/compat/robot_interface.py
Normal 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",
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
"""设置某个电机命令.
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)}
|
||||
|
||||
|
||||
11
include/unitree_legged_sdk/a1_const.h
Normal file
11
include/unitree_legged_sdk/a1_const.h
Normal 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
|
||||
11
include/unitree_legged_sdk/aliengo_const.h
Normal file
11
include/unitree_legged_sdk/aliengo_const.h
Normal 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
|
||||
11
include/unitree_legged_sdk/b1_const.h
Normal file
11
include/unitree_legged_sdk/b1_const.h
Normal 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
|
||||
179
include/unitree_legged_sdk/comm.h
Normal file
179
include/unitree_legged_sdk/comm.h
Normal 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
|
||||
11
include/unitree_legged_sdk/go1_const.h
Normal file
11
include/unitree_legged_sdk/go1_const.h
Normal 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
|
||||
27
include/unitree_legged_sdk/joystick.h
Normal file
27
include/unitree_legged_sdk/joystick.h
Normal 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
|
||||
48
include/unitree_legged_sdk/loop.h
Normal file
48
include/unitree_legged_sdk/loop.h
Normal 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
|
||||
33
include/unitree_legged_sdk/quadruped.h
Normal file
33
include/unitree_legged_sdk/quadruped.h
Normal 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
|
||||
30
include/unitree_legged_sdk/safety.h
Normal file
30
include/unitree_legged_sdk/safety.h
Normal 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
|
||||
62
include/unitree_legged_sdk/udp.h
Normal file
62
include/unitree_legged_sdk/udp.h
Normal 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
|
||||
14
include/unitree_legged_sdk/unitree_legged_sdk.h
Normal file
14
include/unitree_legged_sdk/unitree_legged_sdk.h
Normal 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
|
||||
@@ -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
3
robot_interface.py
Normal 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
50
src/loop.cpp
Normal 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
|
||||
395
src/pro_codec.cpp
Normal file
395
src/pro_codec.cpp
Normal file
@@ -0,0 +1,395 @@
|
||||
#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;
|
||||
constexpr std::size_t kLowStateDecodeSize =
|
||||
((kLowStateParsedSize + 7) / 8) * 8;
|
||||
|
||||
std::array<uint32_t, 256> MakeCrcTable() {
|
||||
std::array<uint32_t, 256> table{};
|
||||
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;
|
||||
}
|
||||
table[byte] = crc;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
const std::array<uint32_t, 256> kCrcTable = MakeCrcTable();
|
||||
|
||||
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) {
|
||||
uint32_t crc = 0xffffffffu;
|
||||
for (std::size_t offset = 0; offset + 4 <= size; offset += 4) {
|
||||
const uint32_t word = GetU32Le(data + offset);
|
||||
crc = (crc << 8) ^ kCrcTable[((crc >> 24) ^ (word >> 24)) & 0xffu];
|
||||
crc = (crc << 8) ^ kCrcTable[((crc >> 24) ^ (word >> 16)) & 0xffu];
|
||||
crc = (crc << 8) ^ kCrcTable[((crc >> 24) ^ (word >> 8)) & 0xffu];
|
||||
crc = (crc << 8) ^ kCrcTable[((crc >> 24) ^ word) & 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 < kLowStateDecodeSize) return false;
|
||||
std::array<uint8_t, kLowStateDecodeSize> data;
|
||||
Decrypt(encrypted, data.data(), data.size());
|
||||
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
|
||||
47
src/pro_codec.h
Normal file
47
src/pro_codec.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#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>
|
||||
|
||||
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
25
src/quadruped.cpp
Normal 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
77
src/safety.cpp
Normal 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
310
src/udp.cpp
Normal 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
|
||||
84
tests/cpp/benchmark_codec.cpp
Normal file
84
tests/cpp/benchmark_codec.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
#include "pro_codec.h"
|
||||
#include "unitree_legged_sdk/unitree_legged_sdk.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
#ifdef GO1_PRO_BENCHMARK_CAPTURE
|
||||
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>());
|
||||
}
|
||||
#endif
|
||||
|
||||
double NanosecondsPerIteration(Clock::time_point start, Clock::time_point end,
|
||||
std::size_t iterations) {
|
||||
return std::chrono::duration<double, std::nano>(end - start).count() /
|
||||
static_cast<double>(iterations);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
constexpr std::size_t kIterations = 200000;
|
||||
go1_pro_internal::ProCodec codec(go1_pro_internal::ProCodec::FindStateFile());
|
||||
|
||||
UNITREE_LEGGED_SDK::LowCmd cmd{};
|
||||
go1_pro_internal::InitLowCmd(cmd);
|
||||
volatile uint64_t checksum = 0;
|
||||
|
||||
for (std::size_t i = 0; i < 100; ++i) {
|
||||
const auto packet = codec.EncodeLowCmd(cmd);
|
||||
checksum += packet[i % packet.size()];
|
||||
}
|
||||
const auto encode_start = Clock::now();
|
||||
for (std::size_t i = 0; i < kIterations; ++i) {
|
||||
const auto packet = codec.EncodeLowCmd(cmd);
|
||||
checksum += packet[i % packet.size()];
|
||||
}
|
||||
const auto encode_end = Clock::now();
|
||||
|
||||
std::cout << std::fixed << std::setprecision(1)
|
||||
<< "EncodeLowCmd: "
|
||||
<< NanosecondsPerIteration(encode_start, encode_end, kIterations)
|
||||
<< " ns/frame\n";
|
||||
|
||||
#ifdef GO1_PRO_BENCHMARK_CAPTURE
|
||||
const auto capture = ReadFile(GO1_PRO_BENCHMARK_CAPTURE);
|
||||
if (capture.size() != go1_pro_internal::kLowStateDatagramSize) {
|
||||
std::cerr << "unexpected benchmark capture size: " << capture.size() << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
UNITREE_LEGGED_SDK::LowState state{};
|
||||
for (std::size_t i = 0; i < 100; ++i) {
|
||||
if (!codec.DecodeLowState(capture.data(), capture.size(), state)) return 1;
|
||||
checksum += state.motorState[i % state.motorState.size()].temperature;
|
||||
}
|
||||
const auto decode_start = Clock::now();
|
||||
for (std::size_t i = 0; i < kIterations; ++i) {
|
||||
if (!codec.DecodeLowState(capture.data(), capture.size(), state)) return 1;
|
||||
checksum += state.motorState[i % state.motorState.size()].temperature;
|
||||
}
|
||||
const auto decode_end = Clock::now();
|
||||
std::cout << "DecodeLowState: "
|
||||
<< NanosecondsPerIteration(decode_start, decode_end, kIterations)
|
||||
<< " ns/frame\n";
|
||||
#else
|
||||
std::cout << "DecodeLowState: skipped (capture fixture unavailable)\n";
|
||||
#endif
|
||||
|
||||
std::cout << "checksum: " << checksum << '\n';
|
||||
return 0;
|
||||
}
|
||||
7
tests/cpp/package_consumer/CMakeLists.txt
Normal file
7
tests/cpp/package_consumer/CMakeLists.txt
Normal 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)
|
||||
9
tests/cpp/package_consumer/main.cpp
Normal file
9
tests/cpp/package_consumer/main.cpp
Normal 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;
|
||||
}
|
||||
138
tests/cpp/test_compat.cpp
Normal file
138
tests/cpp/test_compat.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#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);
|
||||
|
||||
LowState truncated_state{};
|
||||
CHECK(!codec.DecodeLowState(state_packet.data(),
|
||||
go1_pro_internal::kLowStateParsedSize,
|
||||
truncated_state));
|
||||
CHECK(codec.DecodeLowState(state_packet.data(),
|
||||
go1_pro_internal::kLowStateParsedSize + 1,
|
||||
truncated_state));
|
||||
CHECK(truncated_state.motorState[FR_1].temperature ==
|
||||
state.motorState[FR_1].temperature);
|
||||
CHECK(truncated_state.crc == state.crc);
|
||||
|
||||
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;
|
||||
}
|
||||
108
tests/test_capture_fixtures.py
Normal file
108
tests/test_capture_fixtures.py
Normal 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()
|
||||
@@ -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
97
tests/test_mcu_client.py
Normal 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)
|
||||
332
tests/test_official_compat.py
Normal file
332
tests/test_official_compat.py
Normal 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)"
|
||||
Reference in New Issue
Block a user