Skip to main content
Version: QTrobot V3

TypeScript/Node.js API Reference

The @luxai-qtrobot/robot-sdk SDK gives you a single Robot object to control QTrobot V3 from Node.js or directly from a browser — face, speech, gestures, motors, media, and microphone, all through one consistent, Promise-based API.

npm install @luxai-qtrobot/robot-sdk

For plain HTML pages without a build step, drop the UMD bundle in directly from a CDN — no bundler required:

<script src="https://cdn.jsdelivr.net/npm/@luxai-qtrobot/robot-sdk/dist/qtrobot-sdk.umd.js"></script>

All exports are available under the global QTRobotSDK object:

<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/@luxai-qtrobot/robot-sdk/dist/qtrobot-sdk.umd.js"></script>
</head>
<body>
<script>
const { Robot } = QTRobotSDK

// Browsers require WebSocket — use ws:// or wss://, not mqtt://
const robot = await Robot.connectMqtt('ws://10.231.0.1:9001', 'QTRD000320')
console.log(`Connected to ${robot.robotId}`)

await robot.tts.sayText({ text: 'Hello from the browser!' })
</script>
</body>
</html>

On this page: Core concepts · Connecting · Plugins · tts · face · gesture · motor · media · speaker · microphone · camera

Core concepts​

Promises and cancellation​

Every API call is async and returns a Promise — there's no separate blocking/non-blocking pair like some other SDKs. Calls that take real time on the robot (speaking, playing a gesture, playing media) accept an optional signal: AbortSignal, the same pattern used by the browser fetch API, so you cancel by aborting a AbortController:

const controller = new AbortController()

const playPromise = robot.gesture.playFile({ name: 'QT/wave', signal: controller.signal })

// stop it early
controller.abort()

await playPromise // rejects once the cancellation is processed

Failures throw a RobotApiError, so regular try/catch works as expected.

The stream pattern: readers, writers, and callbacks​

Continuous data — joint positions, microphone audio, camera frames — uses streams instead of a single request/response call:

  • Callback — on<Name>(handler, onError?) runs your function for every incoming frame, and returns an unsubscribe function you call when done.
  • Reader — <name>Reader(options?) returns a TypedStreamReader, usable either as an async iterator (for await (const frame of reader)) or with a direct reader.read(timeoutSec) call.
  • Writer — open<Name>Writer() returns a TypedStreamWriter you push frames to: writer.write(frame).
// Callback
const unsubscribe = robot.motor.onJointsState((frame) => {
console.log(frame)
})
// later
unsubscribe()

// Async iterator
for await (const frame of robot.motor.jointsStateReader()) {
console.log(frame)
}

Both the callback and reader forms deliver the exact same data — pick whichever fits your control flow better.

Connecting​

import { Robot } from '@luxai-qtrobot/robot-sdk'

const robot = await Robot.connectMqtt('mqtt://10.231.0.1:1883', 'QTRD000320')
await robot.tts.sayText({ text: 'Hello!' })
robot.close()
ParameterTypeDescription
brokerUrlstringMQTT broker URL, e.g. mqtt://10.231.0.1:1883 or wss://broker:8884/mqtt (browsers must use ws:///wss://).
robotIdstringRobot serial number, e.g. QTRD000320.
optionsConnectOptionsMQTT connection options (TLS, auth, ...) plus connectTimeoutSec and defaultRpcTimeoutSec.

Close the connection with robot.close(), or use a using declaration (TC39 explicit resource management) for automatic cleanup when it goes out of scope:

using robot = await Robot.connectMqtt('mqtt://10.231.0.1:1883', 'QTRD000320')
await robot.tts.sayText({ text: 'Hello!' })
// robot.close() is called automatically here

Robot properties​

PropertyTypeDescription
robot.robotIdstringThe robot's serial number (e.g. QTRD000320).
robot.robotTypestring | nullRobot model/type string.
robot.sdkVersionstring | nullRobot-side SDK version.
robot.minSdk / robot.maxSdkstring | nullSDK version range supported by the robot.

Plugins​

The camera is currently the only plugin available in the TypeScript/Node.js SDK. Once enabled, it appears as robot.camera.

await robot.enablePluginMqtt('camera', 'mqtt://broker:1883', 'QTRD000320/realsense')
const intrinsics = await robot.camera!.getColorIntrinsics()
ParameterTypeDescription
name'camera'Plugin identifier.
brokerUrlstringMQTT broker URL (can be the same broker as the robot).
mqttTopicPrefixstringMQTT topic prefix the plugin is accessible under.
optionsConnectOptionsOptional MQTT + timeout options.

Disable a plugin with robot.disablePlugin('camera').


tts​

robot.tts controls text-to-speech: speaking plain text or SSML, and selecting/configuring engines.

sayText​

Speak plain text. Blocks (the returned Promise resolves) until audio playback completes.

sayText(options: TtsSayTextOptions): Promise<unknown>
ParameterTypeDescription
textstringPlain text to synthesize.
enginestring?Engine id (uses default if omitted).
langstring?Language code, e.g. en-US.
voicestring?Voice id or name.
ratenumber?Speech rate multiplier.
pitchnumber?Pitch adjustment.
volumenumber?Volume level.
stylestring?Speaking style (engine-dependent).
signalAbortSignal?Cancel the operation.
await robot.tts.sayText({ text: 'Hello world!' })
await robot.tts.sayText({ text: 'Slower speech', engine: 'acapela', rate: 0.8, pitch: 1.1 })

// Cancel after 2 seconds
const controller = new AbortController()
const p = robot.tts.sayText({ text: 'A very long sentence...', signal: controller.signal })
setTimeout(() => controller.abort(), 2000)
await p.catch(() => {})

saySsml​

Speak SSML markup. Same cancellation pattern as sayText.

saySsml(options: TtsSaySsmlOptions): Promise<unknown>
ParameterTypeDescription
ssmlstringSSML markup string.
enginestring?Engine id (uses default if omitted).
signalAbortSignal?Cancel the operation.
await robot.tts.saySsml({ ssml: '<speak>Hello!</speak>' })

Engines, voices, and configuration​

MethodReturnsDescription
listEngines()Promise<unknown[]>Loaded/available TTS engine ids.
getDefaultEngine()Promise<string>Current default engine id.
setDefaultEngine({ engine })Promise<unknown>Set the default engine id.
getLanguages({ engine? })Promise<unknown[]>Supported language codes for an engine.
getVoices({ engine? })Promise<unknown[]>Supported voices for an engine.
supportsSsml({ engine? })Promise<boolean>Whether the engine supports SSML.
setConfig({ config, engine? })Promise<unknown>Set engine-specific configuration.
getConfig({ engine? })Promise<Record<string, unknown>>Get engine-specific configuration.
const engines = await robot.tts.listEngines()
await robot.tts.setDefaultEngine({ engine: 'acapela' })

const voices = await robot.tts.getVoices({ engine: 'acapela' })
if (await robot.tts.supportsSsml({ engine: 'azure' })) {
await robot.tts.saySsml({ ssml: '<speak>Hello!</speak>', engine: 'azure' })
}

face​

robot.face controls the face display: emotion playback and eye gaze.

showEmotion​

Show an emotion animation. Blocks until the animation finishes (or is cancelled via signal).

showEmotion(options: FaceShowEmotionOptions): Promise<unknown>
ParameterTypeDescription
emotionstringEmotion name or relative path (with or without .avi).
speednumber?Playback speed factor (uses config default if omitted).
signalAbortSignal?Cancel the operation.
await robot.face.showEmotion({ emotion: 'QT/kiss' })

const controller = new AbortController()
const p = robot.face.showEmotion({ emotion: 'QT/breathing_exercise', signal: controller.signal })
setTimeout(() => controller.abort(), 3000)

listEmotions / look​

listEmotions(): Promise<unknown[]>
look(options: FaceLookOptions): Promise<unknown>
ParameterTypeDescription
l_eyeunknown[][dx, dy] pixel offset from centre for the left eye.
r_eyeunknown[][dx, dy] pixel offset from centre for the right eye.
durationnumber?If > 0, eyes reset to centre after this many seconds.
const emotions = await robot.face.listEmotions()
await robot.face.look({ l_eye: [30, 0], r_eye: [30, 0] })

gesture​

robot.gesture plays predefined or raw keyframe gestures, tracks playback progress, and records new gestures.

play / playFile​

play(options: GesturePlayOptions): Promise<unknown>
playFile(options: GesturePlayFileOptions): Promise<boolean>
ParameterTypeDescription
keyframesRecord<string, unknown>Trajectory dict (for play).
gesturestringGesture name/path, with or without .xml (for playFile).
resampleboolean?Resample trajectory to a uniform rate (play only).
rate_hznumber?Resample rate in Hz (play only).
speed_factornumber?Playback speed multiplier.
signalAbortSignal?Cancel the operation.
await robot.gesture.playFile({ name: 'QT/wave' })

const controller = new AbortController()
const p = robot.gesture.playFile({ gesture: 'QT/bye', signal: controller.signal })
controller.abort() // cancel on demand

listFiles, record, stopRecord, storeRecord​

listFiles(): Promise<unknown[]>
record(options: GestureRecordOptions): Promise<Record<string, unknown>>
stopRecord(): Promise<boolean>
storeRecord(options: GestureStoreRecordOptions): Promise<unknown>
ParameterTypeDescription
motorsunknown[]Motor names to record.
release_motorsboolean?Disable motor torque during recording.
delay_start_msnumber?Delay before recording starts (ms).
timeout_msnumber?Maximum recording duration (ms).
refine_keyframeboolean?Remove redundant keyframes after recording.
keyframe_pos_epsnumber?Position epsilon for keyframe refinement (degrees).
keyframe_max_gap_usnumber?Maximum gap between keyframes (µs).
gesture (for storeRecord)stringName/path to save the recorded gesture as XML.

record() resolves when recording stops (timeout, or signal abort which calls stopRecord for you). For interactive use, abort via signal, or call stopRecord() directly, then storeRecord({ gesture }) to save the result.

const controller = new AbortController()
const recordPromise = robot.gesture.record({
motors: ['RightShoulderPitch', 'RightElbowRoll'],
release_motors: true,
timeout_ms: 20000,
signal: controller.signal,
})
// later, e.g. on a button click
controller.abort()
const keyframes = await recordPromise
await robot.gesture.storeRecord({ gesture: 'my_wave' })

Stream: progress​

onProgress(handler, onError?): () => void
progressReader(options?: { queueSize?: number }): TypedStreamReader<Record<string, unknown>>
const unsubscribe = robot.gesture.onProgress((frame) => {
console.log(frame)
})

// or
for await (const frame of robot.gesture.progressReader()) {
console.log(frame)
}

motor​

robot.motor lists motors, controls torque/homing/velocity, and streams joint state, errors, and commands.

list, torque, and homing​

list(): Promise<Record<string, unknown>>
on(options: { motor: string }): Promise<unknown>
off(options: { motor: string }): Promise<unknown>
onAll(): Promise<unknown>
offAll(): Promise<unknown>
home(options: { motor: string }): Promise<unknown>
homeAll(): Promise<unknown>
setVelocity(options: { motor: string; velocity: number }): Promise<unknown>
const motors = await robot.motor.list()
await robot.motor.onAll()
await robot.motor.home({ motor: 'HeadYaw' })
await robot.motor.setVelocity({ motor: 'HeadYaw', velocity: 50 })
await robot.motor.offAll()

Stream: joint state, errors, and commands​

onJointsState(handler, onError?): () => void
jointsStateReader(options?): TypedStreamReader<JointStateFrame>
onJointsError(handler, onError?): () => void
jointsErrorReader(options?): TypedStreamReader<Record<string, unknown>>
openJointsCommandWriter(): TypedStreamWriter<JointCommandFrame>
  • jointsState — outbound JointStateFrame (position, velocity, effort, voltage, temperature) per motor.
  • jointsError — outbound error flags (overload/voltage/temperature/sensor) per motor, when present.
  • jointsCommand — inbound writer for sending joint position/velocity commands.
const unsubscribe = robot.motor.onJointsState((frame) => console.log(frame))

const writer = robot.motor.openJointsCommandWriter()
writer.write(new JointCommandFrame(...))

media​

robot.media plays audio and video on two independent lanes — foreground (fg) and background (bg) — from files or as live streamed frames. fg and bg expose an identical API; examples below use fg.

File playback​

playFgAudioFile(options: { uri: string; signal?: AbortSignal }): Promise<boolean>
pauseFgAudioFile(): Promise<unknown>
resumeFgAudioFile(): Promise<unknown>
setFgAudioVolume(options: { value: number }): Promise<unknown>
getFgAudioVolume(): Promise<number>
await robot.media.playFgAudioFile({ uri: '/data/audio/hello.wav' })

const controller = new AbortController()
const p = robot.media.playFgAudioFile({ uri: '/data/audio/hello.wav', signal: controller.signal })
setTimeout(() => controller.abort(), 3000)

await robot.media.setFgAudioVolume({ value: 0.8 })

playBgAudioFile / pauseBgAudioFile / resumeBgAudioFile / setBgAudioVolume / getBgAudioVolume mirror the fg methods on the background lane.

Video file playback​

playFgVideoFile(options: { uri: string; speed?: number; with_audio?: boolean; signal?: AbortSignal }): Promise<boolean>
pauseFgVideoFile(): Promise<unknown>
resumeFgVideoFile(): Promise<unknown>
setFgVideoAlpha(options: { value: number }): Promise<unknown>
await robot.media.playFgVideoFile({ uri: '/data/video/intro.mp4' })
await robot.media.setFgVideoAlpha({ value: 0.8 })

playBgVideoFile / pauseBgVideoFile / resumeBgVideoFile mirror the fg methods (no alpha control on bg).

Streamed audio/video​

For audio/video frames you generate yourself, rather than a file on disk:

openFgAudioStreamWriter(): TypedStreamWriter<AudioFrameRaw>
openBgAudioStreamWriter(): TypedStreamWriter<AudioFrameRaw>
openFgVideoStreamWriter(): TypedStreamWriter<ImageFrameRaw>
openBgVideoStreamWriter(): TypedStreamWriter<ImageFrameRaw>

cancelFgAudioStream() / pauseFgAudioStream() / resumeFgAudioStream()
cancelBgAudioStream() / pauseBgAudioStream() / resumeBgAudioStream()
cancelFgVideoStream() / pauseFgVideoStream() / resumeFgVideoStream()
cancelBgVideoStream() / pauseBgVideoStream() / resumeBgVideoStream()
const writer = robot.media.openFgAudioStreamWriter()
await writer.write(new AudioFrameRaw(...))
...
await robot.media.cancelFgAudioStream()

speaker​

robot.speaker controls the robot's overall hardware volume — separate from any individual media lane's volume (see media).

setVolume(options: { value: number }): Promise<boolean>
getVolume(): Promise<number>
mute(): Promise<boolean>
unmute(): Promise<boolean>
await robot.speaker.setVolume({ value: 0.8 })
const vol = await robot.speaker.getVolume()
await robot.speaker.mute()
await robot.speaker.unmute()

microphone​

robot.microphone reads raw audio from the robot's internal 5-channel microphone array (and an optional external mic), and tunes the array's DSP.

DSP tuning​

getIntTuning(): Promise<Record<string, unknown>>
setIntTuning(options: { name: string; value: number }): Promise<boolean>
const params = await robot.microphone.getIntTuning()
await robot.microphone.setIntTuning({ name: 'AGCONOFF', value: 1.0 })

Audio and voice-activity streams​

onIntAudioCh0(handler, onError?): () => void
intAudioCh0Reader(options?): TypedStreamReader<AudioFrameRaw>
// ... ch1, ch2, ch3, ch4 follow the same pattern

onExtAudioCh0(handler, onError?): () => void
extAudioCh0Reader(options?): TypedStreamReader<AudioFrameRaw>

onIntEvent(handler, onError?): () => void
intEventReader(options?): TypedStreamReader<Record<string, unknown>>

Channel 0 is the processed/ASR-ready channel; channels 1–4 are raw per-microphone signals. The external mic stream is only published if enabled in the robot's configuration. The voice-activity/direction-of-arrival event stream carries { activity: boolean, direction: number } (degrees, 0–359).

for await (const frame of robot.microphone.intAudioCh0Reader()) {
process(frame)
}

robot.microphone.onIntEvent((frame) => {
if (frame.activity) console.log('Voice detected — DOA:', frame.direction)
})

camera (plugin)​

robot.camera is the SDK's one plugin namespace so far, exposing the RealSense 3D camera. Enable it first — see Plugins.

Intrinsics​

getColorIntrinsics(): Promise<Record<string, unknown>>
getDepthIntrinsics(): Promise<Record<string, unknown>>
getDepthScale(): Promise<Record<string, unknown>>
const intrinsics = await robot.camera!.getColorIntrinsics()
const { scale } = await robot.camera!.getDepthScale()

Image streams​

onColor(handler, onError?): () => void
colorReader(options?): TypedStreamReader<ImageFrameRaw>
onDepth(handler, onError?): () => void
depthReader(options?): TypedStreamReader<ImageFrameRaw>
onDepthAligned(handler, onError?): () => void
depthAlignedReader(options?): TypedStreamReader<ImageFrameRaw>
onDepthColor(handler, onError?): () => void
depthColorReader(options?): TypedStreamReader<ImageFrameRaw>
StreamDescription
colorColor image (BGR, width × height × 3).
depthRaw depth image (16-bit).
depthAlignedDepth aligned to the color frame.
depthColorFalse-colour depth, colourised for visualisation.
for await (const frame of robot.camera!.colorReader()) {
render(frame)
}

IMU streams​

onGyro(handler, onError?): () => void
gyroReader(options?): TypedStreamReader<unknown[]>
onAcceleration(handler, onError?): () => void
accelerationReader(options?): TypedStreamReader<unknown[]>

Both deliver [x, y, z] — angular velocity in rad/s for gyro, linear acceleration in m/s² for acceleration.

Live media tracks (browser)​

robot.extra (core connection) and robot.camera.extra (camera plugin) expose native WebRTC MediaStreamTracks — only available over a WebRTC connection with use_media_channels: true (the gateway default):

receiveVideoTrack(topic = 'video'): Promise<MediaStreamTrack>
receiveAudioTrack(topic = 'audio'): Promise<MediaStreamTrack>
sendVideoTrack(track: MediaStreamTrack, topic = 'video'): void
sendAudioTrack(track: MediaStreamTrack, topic = 'audio'): void

receive*Track resolves once the remote peer's track arrives — attach it directly to an HTML element:

const audioTrack = await robot.extra.receiveAudioTrack()
audioEl.srcObject = new MediaStream([audioTrack])
audioEl.play()

const videoTrack = await robot.camera!.extra.receiveVideoTrack()
videoEl.srcObject = new MediaStream([videoTrack])
videoEl.play()

send*Track registers a local track (e.g. from getUserMedia) to send to the robot — it must be called from preConnect (see Connecting), before the connection is established:

const robot = await Robot.connectWebrtcMqtt(brokerUrl, robotId, {
preConnect: async (conn) => {
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
conn.sendVideoTrack(stream.getVideoTracks()[0])
},
})

See the TypeScript/Node.js Tutorials for runnable examples, including a full set of browser HTML demos under examples/web/.