Skip to content

Arm Control Example

Control the robot's dual-arm joints in real time via the DDS rt/lowcmd topic using pnd_sdk_python. Message definitions: DDS Message Definitions.

The arm_control_example.py sample is model-agnostic: pick the model with --robot, and it drives only the arm joints (shoulder / elbow / wrist) while all other joints (legs / waist / neck) hold their startup measured angles. Joint order, PD gains, and target poses live in arm_control_config.json in the same directory, so you can tune without touching code.

Unlike the Arm Motion Service Interface (gRPC preset motions), this example sends per-joint MotorCmd for real-time control. Do not mix the two.


Overview

Item Description
Transport DDS pub/sub
Command topic rt/lowcmd (body joint commands)
Feedback topic rt/lowstate (joint state)
Rate 50 Hz (control_dt = 0.02, see config)
Scope Arms (shoulder / elbow / wrist); other joints hold startup angles
Supported models adam_lite / adam_sp / adam_pro / adam_u
SDK pnd_sdk_python

Full low-level walkthrough: DDS Low-Level Motion Example. For ROS 2 middleware, see ROS2 Low-Level Motion Example.


Prerequisites

  1. The reinforcement-learning client startup is completed.
  2. Connect the robot to your computer with an Ethernet cable; for network configuration see Quick Development (Real).
  3. Enter developer mode (handheld LO + RO); RCU blue slow breathing.

Safety

Hang the robot or use an open area for testing, and make sure the area is clear before running. The program moves both arms in the order "reset → spread → lower", pausing for an Enter press before each step.


Obtain Samples

git clone https://github.com/pndbotics/pnd_sdk_python.git
cd pnd_sdk_python && sudo pip3 install -e . --user

Samples under pnd_sdk_python/example/low_level/:

example/low_level/
├── arm_control_example.py      # arm control sample (this page, all models)
├── arm_control_config.json     # joint order / PD / poses config
├── adam_lite/adam_lite_low_level_example.py   # per-model full-body samples
├── adam_sp/adam_sp_low_level_example.py
├── adam_pro/adam_pro_low_level_example.py
└── adam_u/open_arm.py

🔗 pnd_sdk_python


Run (Adam Pro)

Replace enp59s0 with your actual wired interface name:

ip a   # find wired interface name

cd ~/pnd_sdk_python/example/low_level
# --robot: adam_lite / adam_sp / adam_pro / adam_u (default: adam_pro)
# --net:   DDS network interface (default: lo)
python3 arm_control_example.py --robot adam_pro --net enp59s0

The program is interactive: it first asks you to confirm the area around the robot is clear; after you press Enter, the following actions run in order, each requiring an Enter press to continue:

  1. Reset arms: return to the default pose;
  2. Spread arms: open horizontally into a T-pose (spread);
  3. Lower arms: return to the down pose;
  4. Exit control: arm kp ramps to 0 over ~1.5 s, entering a soft damping state, then keeps publishing until you press Ctrl+C.

A background thread keeps publishing lowcmd the whole time (even while idle or after exit) so the command never goes stale; it stops only on Ctrl+C.

Exit developer mode: LT + B on the handheld; RCU returns to purple slow breathing.


Joint Config & Poses

All tunable parameters live in arm_control_config.json; no Python changes required:

Field Description
control_dt Control period (s); 0.02 = 50 Hz
max_joint_velocity Joint velocity limit (rad/s) for pose interpolation
robots Per-model joint order, which defines motor_cmd indices
pd kp / kd per joint
poses default / spread / down arm target poses (rad)

The sample handles two things automatically:

  • Arm joint detection: joints whose names contain shoulder / elbow / wrist are picked as arm joints; the rest (legs, waist, neck) hold their startup measured angles.
  • Model DOF matching: the joint count comes from the robots order in config (e.g. Adam Lite differs from models with wrist pitch/roll), so no manual indices are needed.

To add a custom pose, add a joint-angle set under poses and call move_to(current, arm_target("your_pose")) in the code.


Code Pattern

1. Init communication

from pndbotics_sdk_py.core.channel import (
    ChannelPublisher, ChannelSubscriber, ChannelFactoryInitialize,
)
from pndbotics_sdk_py.idl.default import pnd_adam_msg_dds__LowCmd_
from pndbotics_sdk_py.idl.pnd_adam.msg.dds_ import LowCmd_, LowState_

ChannelFactoryInitialize(1, args.net)
pub = ChannelPublisher("rt/lowcmd", LowCmd_); pub.Init()
sub = ChannelSubscriber("rt/lowstate", LowState_); sub.Init(low_state_handler, 1)

2. Hold non-arm joints: read all measured angles from the first low_state frame as hold targets for non-arm joints.

hold_q = [low_state.motor_state[i].q for i in range(n)]

3. Build command: write mode=1 and hold angles for all joints, then override the arm joints' q with the target pose; on exit, scale arm kp for a soft stop.

def set_cmd(arm_q, arm_kp_scale=1.0):
    for i in range(n):
        cmd.motor_cmd[i].mode = 1
        cmd.motor_cmd[i].q  = float(hold_q[i])
        cmd.motor_cmd[i].kp = float(kp[i])
        cmd.motor_cmd[i].kd = float(kd[i])
    for k, j in enumerate(arm_ids):   # override arm joints only
        cmd.motor_cmd[j].q  = float(arm_q[k])
        cmd.motor_cmd[j].kp = float(kp[j] * arm_kp_scale)

4. Background publishing: a dedicated thread calls pub.Write(cmd) every control_dt so the command never goes stale.

def publisher_loop():
    while True:
        pub.Write(cmd)
        time.sleep(dt)
threading.Thread(target=publisher_loop, daemon=True).start()

5. Smooth move: move_to clamps each joint's increment to max_joint_velocity * dt and steps toward the target pose.

def move_to(current, target):
    while np.max(np.abs(target - current)) > 1e-3:
        current += np.clip(target - current, -max_delta, max_delta)
        set_cmd(current)
        time.sleep(dt)
    return target.copy()

Capability Interface Doc
Real-time upper body (recommended) DDS rt/lowcmd This page
Preset upper-body motion gRPC SetMotion Arm Motion Service
Full-body trajectory tracking gRPC SetTrackingMotion Arm Motion Service
Full-body DDS sample DDS rt/lowcmd low_level.md
Hands DDS rt/handcmd Dexterous Hand Control
ROS 2 alternative lowcmd / handcmd low_level_ros2.md

FAQ

Symptom Possible cause Suggestion
Stuck at "waiting for low_state" Not in developer mode or network down Check LO+RO, subnet, and --net interface
"Unknown model" error --robot value not in config Use adam_lite / adam_sp / adam_pro / adam_u
Arms not responding Kp/Kd is 0 or not in developer mode Check pd in arm_control_config.json; confirm developer mode
Interface not found Wrong interface name Run ip a and pass the correct name