预编译 Python 扩展模块(pybind11)+ URDF/mesh 资源。三层公共面与返回值约定见仓库根 README.md。
模块名:
import oneroarm(pybind11 原生扩展,包名oneroarm)。
python/
├── conda_channel/ # 离线 conda 渠道(x86_64 / arm64 / Windows)
│ ├── linux/{linux-64, linux-aarch64, noarch}/
│ └── windows/{win-64, noarch}/
└── wheels/
└── linux-riscv64/oneroarm-*.whl
类型存根 oneroarm.pyi 由 conda 包 / wheel 自带,安装后位于
<env>/lib/python3.12/site-packages/oneroarm.pyi;预编译扩展位于同目录的
oneroarm.cpython-312-*.so(Linux)或 oneroarm.cp312-*.pyd(Windows)。
oneroarm_description(URDF / mesh)也一并打包到
<env>/lib/python3.12/site-packages/share/oneroarm_description/。
conda create -n oneroarm python=3.12 -y
conda activate oneroarm
# Linux
conda install -c conda-forge -c ./python/conda_channel/linux oneroarm -y
# Windows (PowerShell / cmd)
conda install -c conda-forge -c ./python/conda_channel/windows oneroarm -y
⚠️ 不要用反斜杠.\python\conda_channel\windows。conda 只把开头为./..//~或盘符C:\的字符串识别为本地路径;.\不在此列,会被当成渠道名拼到https://conda.anaconda.org/后面去联网(Windows 必现)。正斜杠在 Windows 上同样有效。若仍想彻底避免歧义,可改用绝对路径或
file://URL(在仓库根目录执行):conda install -c conda-forge -c "file://$PWD/python/conda_channel/linux" oneroarm -y # Linuxconda install -c conda-forge -c "file:///$($PWD.Path -replace '\\','/')/python/conda_channel/windows" oneroarm -y # Windows PowerShell也可用
micromamba install ...替代conda install。安装时必须保留
-c conda-forge,本地 channel 仅提供oneroarm自身,依赖(pinocchio / hpp-fcl / boost / 等)从 conda-forge 拉取。
python -c "import oneroarm; print('OK,', oneroarm.__file__)"无机械臂连接时 OneroArm(cfg) 构造可能不抛异常,但后续 enable_motors() 会返回 False——这是预期行为,可仅用于环境自检。
conda-forge 没有 riscv64 的 oneroarm 及其依赖包,所以 riscv 上不走 conda,
改用随包提供的 linux-riscv64 瘦 wheel:
# 1. 进入仓库根目录,一键准备 RISC-V 第三方依赖
cd /path/to/OneroArm_API_for_Users
sudo ./scripts/install_riscv_dependencies.sh
export LD_LIBRARY_PATH=/opt/onero-deps/lib:$LD_LIBRARY_PATH
# 2. 使用 Python 3.12;wheel 标签是 cp312
python3.12 -m venv .venv_oneroarm
. .venv_oneroarm/bin/activate
python -m pip install --upgrade pip
# 3. 装瘦 wheel(仅含 oneroarm.so + .pyi,不打包第三方动态库)
# --no-deps 用于避免 pip 在 riscv64 上源码编 numpy。
python -m pip install --no-deps ./python/wheels/linux-riscv64/oneroarm-*.whl
# 4. 自检
python -c "import oneroarm; print('OK,', oneroarm.__file__)"⚠ wheel 是「瘦」的:
import oneroarm时由动态加载器从你的LD_LIBRARY_PATH解析libpinocchio_default.so.3.1.0等。版本须严格匹配(pinocchio 必须v3.1.0), 否则报ImportError: ... undefined symbol或cannot open shared object。 排错见第九节同款条目。wheel 文件名中的
cp312/linux_riscv64是 pip 识别兼容性的标准标签,请不要手动改名; 安装时使用上面的oneroarm-*.whl通配符即可。若业务代码确实需要
numpy/loguru,建议优先使用系统包或本地预编 wheel; 直接pip install numpy可能在 riscv64 上触发源码编译,耗时较长且容易缺meson-python。
oneroarm 扩展底层即 liboneroarm.so,URDF/mesh 资源解析优先级(命中即停):
cfg.model_description_path(非空时直接使用)- 环境变量
ONERO_DESCRIPTION_PATH/ONERO_DESCRIPTION_DIR - 自身
dladdr推断 →<site-packages>/share/oneroarm_description/(默认零配置) - CWD 相对回退(兼容历史脚本)
- ROS /
AMENT_PREFIX_PATH
喂给外部工具(RViz / MuJoCo / 自研 viewer)需要绝对路径时:
import os, oneroarm
desc = os.path.join(os.path.dirname(oneroarm.__file__),
"share", "oneroarm_description")或显式 export ONERO_DESCRIPTION_PATH=/path/to/oneroarm_description 由调用方与 SDK 共享同一份资源。
import oneroarm
cfg = oneroarm.OneroConfig() # device / robot_model / dof / version / mit_kp ...
ja = oneroarm.JointArray() # 类 list 容器(也可以直接传 list[float])
p = oneroarm.Pose() # x, y, z, qw, qx, qy, qz
st = oneroarm.ArmStateFromMotor() # positions / velocities / torques
gs = oneroarm.GripperStatus() # position / velocity / force / error / valid
ts = oneroarm.GripperTactileStatus()# sensors[2] / valid
tp = oneroarm.TrajectoryPoint() # position / velocity / acceleration
oneroarm.DragTeachingState.IDLE # 0
oneroarm.DragTeachingState.RECORDING # 1
oneroarm.DragTeachingState.REPLAYING # 2| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
device |
str |
✅ | "" |
串口设备路径。Linux /dev/ttyACM0,Windows COM3 |
robot_model |
str |
✅ | "" |
"a1_l" / "a1_r",子串包含、大小写敏感 |
dof |
int |
– | 7 |
自由度,仅支持 7 |
baud_rate |
int |
– | 921600 |
串口波特率 |
urdf_path |
str |
– | "" |
留空 → 由 model_description_path + version 自动定位 |
version |
str |
– | "" |
URDF 子目录名("A1"),留空时按 robot_model 推断 |
mount_orientation |
str |
– | "vertical" |
"vertical" 立装 / "horizontal" 卧装。装错将导致重力补偿方向错误 |
mit_kp |
list[float] |
– | 全 0 |
MIT 比例增益。==0 视为该关节未传入 |
mit_kd |
list[float] |
– | 全 0 |
MIT 微分增益。同上 |
model_description_path |
str |
– | "" |
oneroarm_description 根目录。留空 → SDK 内置 |
with_gripper |
bool |
– | False |
在同一串口 / CAN 会话内注册可选夹爪;也可构造 OneroArm(cfg, with_gripper=True) |
Python 端不暴露
interrupt_check/interrupt_ctx(C++ 独有)。
7-DOF 默认增益(mit_kp[i] == 0 时自动注入):
| 关节 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
kp |
150 | 150 | 150 | 150 | 30 | 30 | 30 |
kd |
4 | 4 | 4 | 4 | 1 | 1 | 1 |
# MoveResult
0 # SUCCESS
-1 # INVALID_PARAMS
-2 # IK_FAILED
-3 # COLLISION_DETECTED
-4 # EXECUTION_FAILED
-5 # TIMEOUT
-6 # INTERRUPTED
-7 # JOINT_LIMIT_EXCEEDED
-8 # BUSY
# 原始 CAN 帧
0 # ONERO_CAN_OK
-10 # ONERO_ERR_RAW_FRAME_INVALID_LEN
-11 # ONERO_ERR_RAW_FRAME_INVALID_ID
-12 # ONERO_ERR_RAW_FRAME_RESERVED_ID
-13 # ONERO_ERR_RAW_FRAME_PORT_NOT_OPEN
-14 # ONERO_ERR_RAW_FRAME_SEND_FAILEDclass OneroArm:
def __init__(self, config: OneroConfig, with_gripper: bool = False) -> None: ...构造时按 OneroConfig 创建底层 robot handle;Python 层无 valid(),构造失败会抛异常。运动 / 状态方法既接受 JointArray,也接受 list[float](自动转换)。
| 方法 | 返回 | 说明 |
|---|---|---|
enable_motors() -> bool |
True 成功 |
同步等待硬件应答;首次使能可能耗时秒级 |
disable_motors() -> bool |
True 成功 |
同步阻塞 |
restore_arm() -> int |
MoveResult |
以安全速度回默认零位 |
restore_arm(target) -> int |
MoveResult |
以安全速度回指定关节目标 |
Python 的
enable_motors()/disable_motors()仍返回bool:True即成功,False即任何非 0 错误码。
选配,默认关闭。with_gripper=True 时,arm.gripper 是 arm-owned 控制器;默认 False 时为 None。夹爪随臂初始化、复用 OneroArm 的同一串口 / CAN 会话(固定 CAN ID 0x08/0x18),不会单独打开设备;arm.enable_motors() 只使能机械臂关节,夹爪需显式 arm.gripper.enable()。
| 成员 / 方法 | 参数 | 返回 | 语义 |
|---|---|---|---|
OneroArm(cfg, with_gripper=True) / cfg.with_gripper=True |
– | OneroArm |
随臂创建可选夹爪 |
arm.has_gripper() |
– | bool |
是否随臂创建了夹爪 |
arm.gripper |
属性 | Optional[OneroGripper] |
夹爪控制器;未开启时为 None |
gripper.enable() / gripper.disable() |
– | bool |
使能 / 失能夹爪电机(固定 ID 0x08/0x18,与 enable_motors() 解耦) |
gripper.set_position(percent) |
percent:0..100 |
int(MoveResult) |
单帧位置保持 |
gripper.move_position(percent, max_vel=100.0, max_acc=250.0, max_jerk=1000.0) |
percent:0..100;速度 / 加速度 / 加加速度上限 |
int |
100 Hz 点到点规划 |
gripper.force_control(torque) |
torque:N |
int |
下发 MIT 力矩,内部钳位 ±40 N |
gripper.status() |
– | GripperStatus |
刷新并返回 position / velocity / force / error / valid |
gripper.get_tactile() |
– | GripperTactileStatus |
刷新并返回两个触摸传感器各自的合力与 9 个测点快照 |
arm = oneroarm.OneroArm(cfg, with_gripper=True)
arm.gripper.enable()
arm.gripper.set_position(50.0) # percent, 单帧保持
arm.gripper.move_position(80.0, 100.0, 250.0, 1000.0) # 100 Hz 点到点规划
arm.gripper.force_control(30.0) # limited to +/-40 N
status = arm.gripper.status()
print(status.position, status.velocity, status.force, status.error)
tactile = arm.gripper.get_tactile()
if tactile.valid:
for sensor in tactile.sensors:
if sensor.valid:
f = sensor.total_force
print(sensor.sensor_id, f.fx, f.fy, f.fz)
for p in sensor.points:
print("point", p.point_id, p.fx, p.fy, p.fz)status.position / status.velocity 使用用户侧百分比单位;内部会映射到夹爪工作范围。force_control() 和 status.force 使用 N,夹爪力矩命令限制为 ±40 N。夹爪状态和故障码只属于夹爪域,不会改变机械臂 dof 或关节状态缓存。
get_tactile() 当前读取传感器 0x01 和 0x02 的 0x00..0x09:0x00 写入 sensor.total_force,0x01..0x09 写入 sensor.points。fx/fy 按 int8_t * 0.1N 解析,fz 按 uint8_t * 0.1N 解析。
def movej(self, target: list[float] | JointArray,
speed_scale: float = 1.0, trajectory_connect: int = 0) -> int: ...
def movel(self, pose: Pose,
speed_scale: float = 1.0, trajectory_connect: int = 0) -> int: ...
def movep(self, pose: Pose,
speed_scale: float = 1.0, trajectory_connect: int = 0) -> int: ...
def estimate_movej_duration(self, target: list[float] | JointArray,
speed_scale: float = 1.0) -> float: ...返回 MoveResult(0=成功,负数错误码见 §4.2)。语义、参数约束(speed_scale 建议 (0, 2.0],trajectory_connect 0 立即/1 缓冲)与 C++ 一致。
def send_trajectory_point (self, q: JointArray, dq: JointArray) -> int: ...
def send_trajectory (self, trajectory: list[TrajectoryPoint]) -> int: ...
def execute_buffered_trajectory(self) -> int: ...
def clear_trajectory_buffer (self) -> int: ...
def reset_stop_signal (self) -> None: ...
def cancel_trajectory (self) -> int: ...def control_mit(self, kp: JointArray, kd: JointArray, q: JointArray,
dq: JointArray, tau: JointArray) -> int: ...
def compute_gravity_torque(self, q: JointArray) -> JointArray: ...低层力位混合(阻抗)接口,用于 teleop 数据采集、阻抗控制、模仿学习推断等。控制律 tau_motor = kp*(q - q_act) + kd*(dq - dq_act) + tau 由电机在 MIT 模式下闭环执行。调用前先 enable_motors();所有数组长度 = dof,需以 ≥100 Hz 持续下发,不要与 movej/movel/movep 在重叠时间窗内混用。
control_mit(...):返回int(0成功 /-1参数错误 /-2硬件未初始化 /-3至少一关节 CAN 写入失败);调用期间释放 GIL。q走与get_arm_state_from_motor()一致的 SDK 关节空间,建议第一帧q=当前回读位置、dq=0、tau=0。compute_gravity_torque(q):返回值即重力补偿力矩JointArray(含robot_model校准缩放),可直接作为control_mit的tau;len(q) != dof或动力学模型未就绪时抛ValueError。注意此处返回 tau,与 C / C++ 的「int返回 + 出参」风格不同。
| 方法 | 数据源 | 是否触发 CAN I/O | 适用场景 |
|---|---|---|---|
get_joint_positions() -> JointArray |
上位机内部缓存 | 否 | 通用查询,最低开销 |
get_joint_positions_from_motors() -> JointArray |
电机回读 | 与外部传感器对齐 | |
get_joint_velocities() -> JointArray |
电机回读 | 速度环 / 监督控制 | |
get_arm_state_from_motor() -> ArmStateFromMotor |
电机回读 | 一次性获取位置 + 速度 + 力矩 | |
get_arm_state_cached() -> ArmStateFromMotor |
控制循环缓存 | 否 | 推荐用于 GUI / 数据采集线程的高频轮询 |
get_end_effector_pose() -> Pose |
实时状态 / 内部正运动学 | 可能 | 末端位姿读取 |
get_end_effector_pose_cached() -> Pose |
控制循环缓存 | 否 | 高频只读位姿查询 |
is_hardware_connected() -> bool |
总线心跳 | 否 | 启动 / 故障检测 |
失败时
JointArray长度为 0、Pose全零、ArmStateFromMotor三个JointArray同时为 0;调用方应用len(...)防御。
class OneroDragTeaching:
def __init__(self) -> None: ...
def initialize (self, dof: int, record_file: str, time_step: float = 0.01) -> bool: ...
def set_hardware(self, device: str, urdf_path: str, robot_model: str,
mount_orientation: str = "horizontal",
with_gripper: bool = False) -> bool: ...
def enable_motors (self) -> int: ...
def restore_arm (self) -> int: ...
def restore_arm (self, target: list[float] | JointArray) -> int: ...
def start_recording(self) -> int: ...
def stop_recording (self) -> int: ...
def set_replay_file(self, replay_file: str) -> None: ...
def start_replay (self) -> int: ...
def stop_replay (self) -> int: ...
def handle_command (self, cmd: int) -> int: ...
@staticmethod
def handle_command_dual(left: "OneroDragTeaching",
right: "OneroDragTeaching",
cmd: int) -> int: ...
def timer_callback (self) -> None: ...
def get_state (self) -> DragTeachingState: ...
def is_initialized (self) -> bool: ...
def update_joint_state(self, position: JointArray,
velocity: JointArray, effort: JointArray) -> None: ...典型流程:
drag = oneroarm.OneroDragTeaching()→drag.initialize(7, "/tmp/teach.csv", 0.01)→drag.set_hardware(...)。drag.enable_motors();如需先回零,再显式调用drag.restore_arm()。- 录制:
drag.start_recording()→ 物理拖动 →drag.stop_recording()。 - 回放:
drag.set_replay_file(path)→drag.start_replay()→ 必要时drag.stop_replay()。 - 周期循环:100 Hz 调用
drag.timer_callback(),并把电机回读drag.update_joint_state(...)喂入。
双臂同步命令:
OneroDragTeaching.handle_command_dual(left, right, cmd) 接受两个已初始化并完成 set_hardware(...) 的拖动示教实例。cmd=1 会在 SDK 内部对两臂执行并发 prepare,并用共享 t0 同步开始录制;cmd=3 走同步回放路径;cmd=0/2 分别用于双臂停止、停止录制。Python 侧不需要自己计算或传入 steady_clock 时间点。
最小运动序列(与 demo/arm_control_demo.py 同源):
import oneroarm
cfg = oneroarm.OneroConfig()
cfg.device = "/dev/ttyACM0" # Windows: "COM3"
cfg.robot_model = "a1_r"
cfg.version = "A1"
cfg.dof = 7
# model_description_path 留空 → 走 SDK 内置;如需切换可显式赋值
arm = oneroarm.OneroArm(cfg)
assert arm.enable_motors(), "enable_motors failed"
# 关节空间
arm.movej([0.0] * 7, speed_scale=0.5)
# 笛卡尔直线
p = oneroarm.Pose()
p.x, p.y, p.z = 0.30, 0.00, 0.40
p.qw = 1.0
arm.movel(p, speed_scale=0.5)
# 状态查询
st = arm.get_arm_state_cached()
if len(st.positions) > 0:
print("q[0] =", st.positions[0])
arm.disable_motors()与电机 / 夹爪共用同一根 SLCAN 串口;可用于向同总线上的自定义节点(MCU、传感器、IO 板等)发送 11-bit 标准 CAN 帧并接收响应。共同语义(保留 ID 集、回调线程、payload 生命周期、异常处理)见根 README.md §7。
class OneroArm:
def send_can_frame(self, can_id: int,
payload: bytes | bytearray | memoryview) -> int: ...
def register_can_frame_callback(
self,
callback: Callable[[int, bytes], None]) -> int: ...
def clear_can_frame_callback(self) -> int: ...
def pump_can_bus(self, timeout_ms: int) -> int: ...| 方法 | 参数 / 行为 |
|---|---|
send_can_frame(can_id, payload) |
can_id ∈ [0, 0x7FF],不能落在保留集(含 arm 电机、夹爪 0x08/0x18、触觉回包 0x418、0x7FF);payload 接受 bytes / bytearray / memoryview 等任何 buffer-like 对象,长度 ≤ 8。同步发送,返回时 SDK 已拷贝 payload,调用方可立即释放底层缓冲 |
register_can_frame_callback(callback) |
重复注册替换前一个;传 None 等价 clear_can_frame_callback()。SDK 在派发前会拷贝 payload 为 bytes,因此回调结束后 payload 仍然有效;回调内禁止重入 arm 任何发送 / 运动控制方法。回调中抛出的异常会被 SDK 静默吞掉,业务侧应自行 try/except |
clear_can_frame_callback() |
清除已注册的回调(user_data 不再被引用) |
pump_can_bus(timeout_ms) |
timeout_ms == 0 = 一次非阻塞 try-recv;pump_can_bus 内部会释放 GIL,让其他 Python 线程在等待期间继续工作。回调本身在重新获取 GIL 后执行 |
import oneroarm
cfg = oneroarm.OneroConfig()
cfg.device = "/dev/ttyACM0"
cfg.robot_model = "a1_r"
cfg.dof = 7
arm = oneroarm.OneroArm(cfg)
def on_frame(can_id: int, data: bytes) -> None:
try:
print(f"rx 0x{can_id:X} len={len(data)} {data.hex()}")
except Exception as e:
print("user callback error:", e) # SDK 会吞掉未捕获异常
arm.register_can_frame_callback(on_frame)
rc = arm.send_can_frame(0x100, b"\x01\x02\x03\x04") # 0 = 成功
if rc != 0:
print(f"send failed: {rc}")
arm.pump_can_bus(50) # 50 ms 内派发到回调
arm.clear_can_frame_callback()| 现象 | 原因 / 处理 |
|---|---|
PackagesNotFoundError: oneroarm |
用实际相对/绝对路径作为 -c channel;python/conda_channel/linux/ 下应有 linux-64/ + noarch/ 子目录及 repodata.json |
UnsatisfiableError / LibMambaUnsatisfiableError |
创建全新环境:conda create -n oneroarm python=3.12 -y;安装命令必须保留 -c conda-forge |
ImportError: DLL load failed(Windows) |
确认已 conda activate oneroarm,IDE 解释器是该环境的 Python |
OSError: cannot find liboneroarm 或 Symbol not found |
没安装传递依赖;conda env 内 conda install -c conda-forge pinocchio hpp-fcl boost 重装 |
RISC-V pip install 卡在编 numpy / 报 Cannot import 'mesonpy' |
RISC-V wheel 安装用 python -m pip install --no-deps ...linux_riscv64.whl;numpy/loguru 如确实需要再单独用系统包或本地 wheel 安装 |
RISC-V ImportError: libpinocchio_default.so.3.1.0: cannot open shared object file |
未加载 /opt/onero-deps/lib;先执行 export LD_LIBRARY_PATH=/opt/onero-deps/lib:$LD_LIBRARY_PATH,并确认 install_riscv_dependencies.sh 已完成 |
RISC-V wheel 报 not a supported wheel on this platform |
Python 版本或平台不匹配;当前 wheel 是 cp312 + linux_riscv64,请用 python3.12 且在 riscv64 系统安装 |
Serial device not found |
Linux:ls /dev/ttyACM*,必要时 sudo chmod 666 /dev/ttyACM0 或加入 dialout 组。Windows:设备管理器查 COM 端口号,更新 cfg.device |
enable_motors() 返回 False |
检查串口设备路径、波特率、急停按钮;先 arm.is_hardware_connected() 验证 |
send_can_frame 返回 -12 |
can_id 落在保留集(电机 / 夹爪 / 触觉回包 / 操纵杆 / 0x7FF) |
| 回调收不到帧 | 对端发送的 CAN ID 不能在 SDK 保留集;运动控制空闲期主动 arm.pump_can_bus(timeout_ms) |
| 回调中的异常被吞掉、看不到 traceback | 在回调内自己 try/except 并打印 / 写日志 |
启动报 Pinocchio model load failed / 找不到 URDF |
默认走 SDK 内置;若被覆盖可设 ONERO_DESCRIPTION_PATH 或 cfg.model_description_path |