Skip to main content
The Virtual Controller (virtual_controller) resolves a command string against the keyboard controller policy assigned to a digital twin and publishes the matching actuation on cyberwave/twin/{uuid}/command. The MQTT payload matches keyboard teleoperation (KeyboardTeleoperationController / useMQTTTwin.publishCommand) — nothing is hardcoded per robot; bindings come from the twin’s policy at runtime.
Palette category: Actuation. Runs on cloud (Celery MQTT) and edge (worker client.mqtt.publish). Locomotion uses the MQTT command topic, not WebRTC; media streaming stays on separate channels.

Voice-to-actuation pipeline

audio_track
  → audio_assistant (optional VAD)
  → wake_word_engine (optional)
  → call_model (STT)
  → fuzzy_matcher ← twin (control_actuations / control_labels)
  → conditional (optional, branch on matched)
  → virtual_controller
       → cyberwave/twin/{uuid}/command
StepNodeKey wiring
1TwinSelect target twin → wire control_actuations → Fuzzy Matcher candidates
2Call ModelSTT result → Fuzzy Matcher query
3Fuzzy Matchermatch_string → Virtual Controller command (when match is true)
4Virtual ControllerSame twin as Twin node → publishes resolved actuation

Inputs

FieldTypeRequiredDescription
twin_uuidstringYesTwin whose controller policy resolves commands. Set in inspector (Twin combobox).
commandstringYesLabel or actuation to dispatch. Wire from Fuzzy Matcher match_string or another upstream string.
Precedence: wired input mapping → node parameters.

Command matching

The node accepts either form (normalization is shared with Fuzzy Matcher):
InputResolves to actuation
camera_rightcamera_right
Camera Rightcamera_right
W (single key)binding for that key
Resolution order:
  1. Exact actuation (case / punctuation insensitive)
  2. Exact human label
  3. Keyboard key
  4. Substring containment (label inside longer STT phrase)
Bindings are collected from:
  • metadata.keyboard_bindings (actuation, label, key, optional playground data)
  • metadata.display_config.widgets (button command, slider command)
  • Universal keyboard policies with supports_locomotion and no bindings → default WASD locomotion actuations (move_forward, turn_left, …)

Inspector

  • Digital Twin — combobox; policy is never hardcoded in the node.
  • command — wire from upstream (typically Fuzzy Matcher match_string).

Inspector preview

When a twin is selected, the panel shows:
SectionContents
TwinName, slug, UUID, environment, asset UUID
ControllerPolicy name, type (teleop), control mode (command / velocity / joint_control), input device, asset slug, policy UUID
Forwardable commandsScrollable list: actuation slug, label, optional key
Use this list to verify what voice commands the pipeline can dispatch before running the workflow.

MQTT payload

Published to cyberwave/twin/{twin_uuid}/command:
{
  "source_type": "tele",
  "command": "move_forward",
  "data": {},
  "timestamp": 1710000000.0
}
FieldCloud workflowEdge worker
source_typetele (live) or sim_tele (simulation execution target)edge
commandResolved actuation slug (not the raw input label)
dataFrom binding playground when present, else {}
Same shape as manual keyboard teleop so edge drivers and simulators consume workflow commands without special cases.

Outputs

FieldTypeDescription
commandstringResolved actuation that was last sent on MQTT
available_commandsarray{actuation, label, key} for every forwardable binding
twin_namestringTwin display name
controller_namestringPolicy name
controller_typestringe.g. teleop
matchedbooleantrue when resolution succeeded
sentbooleanCloud: MQTT queued; edge: publish attempted
On resolution failure the node raises an error (unknown command, missing policy, missing twin).

Execution targets

TargetBehavior
Cloudrun_virtual_controller_nodesend_update_to_mqtt.delay
EdgeEmitter caches policy per twin (_CW_VC_POLICY_CACHE) via API on first use; client.mqtt.publish with QoS 0
Edge workers save the resolved controller snapshot (UUID, name, metadata, parsed entries) in process memory so repeat commands on the same twin do not re-fetch the policy every frame. Only run Virtual Controller when Fuzzy Matcher succeeded:
fuzzy_matcher.match → conditional (equals true) → virtual_controller
Pass fuzzy_matcher.match_string into virtual_controller.command on the true branch.

Dependencies

LayerRequirement
Compile serverNo ML extras. paho-mqtt and httpx are base Django/SDK deps.
Edge workerBase cyberwaveclient.twins.get, controller-policies API, client.mqtt.publish
edge-syncNo model_requirements (no ML weights)
rapidfuzz is only required on the Fuzzy Matcher node upstream, not on Virtual Controller. Full voice-pipeline matrix: Edge workflow dependencies.

Controller policy examples

Command-mode UGV policies (control_mode: "command") define bindings like:
{
  "key": "W",
  "label": "Forward",
  "actuation": "move_forward",
  "continuous": true
}
Virtual Controller sends move_forward on MQTT when the pipeline passes "Forward", "move_forward", or fuzzy-matched variants. See seeded policies in seed_controllers.py (e.g. controller:ugv-beast:v1). Universal keyboard (controller:keyboard-locomotion:v1) has no per-asset bindings in seed data; the node uses built-in locomotion defaults when supports_locomotion is true.
  • Twin — supplies Fuzzy Matcher candidates from control_actuations / control_labels
  • Fuzzy Matcher — maps noisy STT (uncertain string) → match_string from a source-of-truth string or array
  • Call Model — STT text → Fuzzy Matcher query
  • Conditional — gate on matched
  • Send MQTT — generic publish; Virtual Controller is purpose-built for controller actuations