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 object — aim_at_pixel()/aim_at_point() to gesture at something the camera has located.
  • Pure geometry, no motion — pixel_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.