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:

  • Callbackon<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.
  • Writeropen<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/.