Skip to main content
Version: QTrobot V3

QTrobot Motion and Actuators

QTrobot has a full internal metal structure and eight motors with metal gearboxes. Thanks to its advanced compliance motor controllers, QTrobot has flexible joints with overload protection, offering a robust platform for intensive working hours. The smart motors also provide a variety of feedback, including position, velocity, and effort. Combined with QTrobot's gesture and kinematics SDKs, this gives you a flexible foundation for everything from recording and playing gestures to commanding individual joints or driving the arms and gaze in cartesian space.

display

Software interface

Motion is handled by the motor node in qtrobot-service-hub (ZMQ port 50540), with two layers on top of it:

  • robot.motor: low-level joint control — list motors, torque on/off, homing, velocity, joint state/error/command streams.
  • robot.gesture: built-in keyframe playback and recording, implemented directly on top of the motor node.
  • robot.kinematics (plugin, Python only currently): inverse kinematics for head gaze and arm reach/aim in cartesian space.

QTrobot supports two motor controller hardware families, Feetech and Herkulex, depending on the robot's hardware revision — the motor node config lives under /opt/luxai/qtrobot_service_hub/etc/motor/feetech/ or .../motor/herkulex/, with one file per joint (head_yaw.yaml, right_shoulder_pitch.yaml, ...) plus a top-level motor.yaml listing the active joints and pointing at them. Which family your robot uses is already configured at the factory — you shouldn't need to change it.

See the Motor, Gesture and Kinematics tutorials, and the Python, TypeScript/Node.js and ROS2 API references for full code.

Joint configuration and limits

QTrobot's joints are grouped into three parts: head, right_arm and left_arm. Each joint's range of motion is enforced both in its per-joint config (pos_limits) and at the embedded motor-controller level — so even a buggy command can't push a joint past its physical limit:

  • head
    • HeadYaw [min: -60.0, max: 60.0]
    • HeadPitch [min: -25.0, max: 25.0]
  • right_arm
    • RightShoulderPitch [min: -140.0, max: 140.0]
    • RightShoulderRoll [min: -75.0, max: -5.0]
    • RightElbowRoll [min: -80.0, max: -5.0]
  • left_arm
    • LeftShoulderPitch [min: -140.0, max: 140.0]
    • LeftShoulderRoll [min: -75.0, max: -5.0]
    • LeftElbowRoll [min: -80.0, max: -5.0]
warning

These are per-joint software limits, not collision avoidance — QTrobot does not have self-collision awareness. Each joint individually stays within its limit, but a trajectory combining several joints can still bring parts of the robot into contact with each other.

Reading motor feedback and commanding motors

def on_state(frame):
print(frame.position("HeadYaw"), frame.velocity("HeadYaw"))

robot.motor.stream.on_joints_state(on_state)

robot.motor.set_velocity("HeadYaw", 50)

from luxai.robot import JointCommandFrame
writer = robot.motor.stream.open_joints_command_writer()
cmd = JointCommandFrame()
cmd.set_joint("HeadYaw", position=30, velocity=40)
writer.write(cmd)

A command always targets a specific named joint — there's no "send to all joints at once" call; send one command per joint you want to move (you can send several in a row without waiting for each to finish). robot.motor.list() returns every configured motor along with its position_min/position_max/position_home/velocity_max/overload_threshold.

Gestures

robot.gesture plays and records predefined keyframe gestures. QTrobot comes with pre-recorded gestures, located and categorized on QTRP under /home/qtrobot/robot/data/gestures. QTrobot can only play one gesture at a time — a new play request while one is already running is rejected, not queued.

gestures = robot.gesture.list_files()

h = robot.gesture.play_file_async("QT/bye")
h.cancel() # if needed

Gesture file format

Gesture files are mostly unchanged from earlier QTrobot generations: an XML file under /home/qtrobot/robot/data/gestures (e.g. QT/bye.xml) containing joint position waypoints with timestamps, plus which robot parts are involved and the gesture's total duration:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<gesture>
<name>QT/bye</name>
<parts>
<part>right_arm</part>
</parts>
<duration>5.54</duration>
<waypoints count="97">
<point time="1558971402671926152">
<RightElbowRoll>-31.90</RightElbowRoll>
<RightShoulderPitch>-88.60</RightShoulderPitch>
<RightShoulderRoll>-59.30</RightShoulderRoll>
</point>
<point time="1558971402704626777">
<RightElbowRoll>-30.60</RightElbowRoll>
<RightShoulderPitch>-88.60</RightShoulderPitch>
<RightShoulderRoll>-60.60</RightShoulderRoll>
</point>
...
</waypoints>
</gesture>

A gesture can involve all parts or just one (e.g. only head). Files can be hand-edited after recording — for example, to trim a trailing delay by deleting the unneeded waypoints at the end.

Recording a gesture

Recording works by releasing torque on the motors you want to capture, moving them by hand, and letting the SDK record the resulting trajectory, using the Python/TypeScript/ROS2 SDKs shown below.

h = robot.gesture.record_async(
motors=["RightShoulderPitch", "RightShoulderRoll", "RightElbowRoll"],
release_motors=True,
delay_start_ms=2000,
timeout_ms=20000,
)
input("Press Enter to stop recording...")
robot.gesture.stop_record()
keyframes = h.result()

robot.gesture.play(keyframes) # play it back immediately
robot.gesture.store_record("my_wave") # or save it for later playback with play_file()
  • motors — the list of motors to record; only their movement is captured.
  • release_motors=True — disables torque on those motors for the duration of the recording, so they can be moved freely by hand. This is almost always what you want when recording by hand.
  • delay_start_ms — recording starts this many milliseconds after the call, giving you time to get into position.
  • timeout_ms — a safety cap: recording stops automatically even if stop_record() is never called.

There's no SDK call to delete a stored gesture — remove its XML file from /home/qtrobot/robot/data/gestures directly to do that.

Keyframe smoothening

Two independent smoothing mechanisms apply at different stages:

  • At recording time, refine_keyframe=True (plus keyframe_pos_eps/keyframe_max_gap_us) compresses the raw, densely-sampled recording by dropping redundant keyframes that don't meaningfully change the trajectory — useful if you plan to hand-edit the resulting file.
  • At playback time, play()/play_async() resample the trajectory by default (resample=True, rate_hz=100.0) to produce smooth motion regardless of how the original keyframes were spaced; speed_factor scales overall playback speed.
# Smoother, slightly compressed recording
robot.gesture.record(motors=[...], release_motors=True, refine_keyframe=True)

# Play back at half speed with a higher resample rate
robot.gesture.play(keyframes, rate_hz=200.0, speed_factor=0.5)
Tips for recording better gestures

Never let robot parts hit each other or the robot's own body while recording.

Move the arms/head slowly and smoothly — sharp, fast movements are automatically slowed down by a safety feature, which will distort the gesture you actually wanted to record.

Avoid fast up/down head movements; they can apply excessive force to the neck joints when the gesture is played back.

Recording is easier with an assistant: one person driving the app/SDK, the other moving the robot's parts.

Kinematics (inverse kinematics plugin)

For applications that think in terms of 3-D positions rather than individual joint angles, the robot.kinematics plugin (Python only, currently) solves inverse kinematics for head gaze and arm reach/aim, working in the robot's base frame: origin at the bottom of the robot, x forward, y left-positive, z up, all in metres.

robot.enable_plugin_local("kinematics")

Look at a point (head gaze)

robot.kinematics.look_at_point(1.0, 0.0, 0.6)               # look straight ahead
robot.kinematics.look_at_point(1.0, -0.5, 1.5) # look to the right
robot.kinematics.look_at_point(1.0, -0.2, 0.6, only_gaze=True) # eyes only, no head motion
robot.kinematics.look_at_pixel(320, 240, depth=1.0) # look at a camera pixel

Aim/reach at a point (arm)

robot.kinematics.reach_right(0.3, -0.2, 0.5)   # explicit right arm
robot.kinematics.reach_left(0.3, 0.2, 0.5) # explicit left arm

robot.kinematics.aim_at_point(0.5, 0.0, 1.0) # arm auto-selected by sign of y
robot.kinematics.aim_at_pixel(320, 240, depth=1.0)

aim_at_* auto-selects the arm by the sign of y (left arm if y >= 0, right arm if y < 0); reach_left/reach_right let you pick explicitly. Points outside the reachable workspace are clamped to the closest reachable configuration rather than failing outright. Every call has an _async counterpart returning an ActionHandle for non-blocking use (.wait()/.cancel()).

Typical use cases

  • Looking toward a detected face or sound source — combine kinematics.look_at_pixel() with a face/object detection result from the camera, or with the microphone's direction-of-arrival event, to make QTrobot turn toward whoever it's interacting with.
  • Pointing at or reaching toward an objectaim_at_pixel()/aim_at_point() to gesture at something the camera has located.
  • Pure geometry, no motionpixel_to_point() converts a camera pixel to a 3-D point in the base frame without moving any motor, useful if you just need the coordinate for your own logic (e.g. logging, deciding whether to react before committing to a movement).

robot.kinematics.configure(...) lets you override the camera intrinsics or motor-wait timeout it uses internally — the defaults already match QTrobot's hardware, so this is rarely needed.

QTrobot URDF

The URDF (Universal Robot Description Format) describes a robot's physical structure: motor positions/orientations, the links between them, and so on. It's useful any time you need that description outside of QTrobot's own software — for example, for visualization, simulation, or motion planning in your own external tooling.

display

Download the QTrobot URDF files here: qtrobot.pdf and qtrobot_urdf.zip.