Skip to content

Robot Kit

The robotics toolkit: drive, sense, look, listen, speak, emote tools and the independent safety monitor.

The Robot Kit (crates/robot-kit) turns a Revka agent into the brain of a physical robot. It is a standalone Rust crate that exposes six agent tools — drive, sense, look, listen, speak, and emote — plus an independent SafetyMonitor that can override any movement the AI requests. It is designed for Raspberry Pi deployment with offline inference (Ollama for vision, Whisper.cpp for hearing, Piper for speech), but every tool has a mock mode so you can develop and test with no hardware attached.

Use this page when you want to build an embodied robot — a rover, a toy, a patrol bot — controlled by natural-language agent calls. For tethered microcontroller GPIO (Pico, Arduino, Nucleo) and the Aardvark adapter, see GPIO tools and Aardvark I2C/SPI/GPIO instead. For running Revka itself on a Pi, see Raspberry Pi (self-hosted).

The Robot Kit sits between the agent loop and the hardware. The agent calls tools; every drive command is funnelled through the SafetyMonitor, which runs as an independent task and has final say on whether the robot moves.

Revka AI Brain ──tool calls──▶ Robot Kit tools
drive · look · listen · speak · sense · emote
SafetyMonitor (independent task)
pre-move check · speed limiting · bump · watchdog · E-stop
Hardware: motors · camera · mic · speaker · LIDAR · LEDs

The guiding principle, from the crate itself: the AI can REQUEST movement, but the SafetyMonitor ALLOWS it. Safety always wins, even if the model hallucinates.

The kit defines its own lightweight Tool trait so it can be used standalone, but the tools are compatible with Revka’s agent loop. The crate is a workspace member and is not auto-registered in the core runtime — you wire it in from Rust (see Wiring it into an agent).

Build the crate as a workspace package:

Terminal window
cargo build -p revka-robot-kit --release
FeatureDefaultWhat it enables
safetyonThe SafetyMonitor background task and the SafeDrive wrapper. Strongly recommended; keep it enabled.
gpiooffDirect GPIO control via rppal (Raspberry Pi / Linux only).
ros2offROS2 integration build flag.
lidaroffLIDAR support build flag.
visionoffCamera + vision-model build flag.

All hardware and limits live in robot.toml. Copy the template from the crate and edit it for your robot:

Terminal window
mkdir -p ~/.revka
cp crates/robot-kit/robot.toml ~/.revka/

A minimal, mock-mode config looks like this:

[drive]
backend = "mock" # "ros2" | "serial" | "gpio" | "mock"
ros2_topic = "/cmd_vel"
serial_port = "/dev/ttyACM0"
max_speed = 0.5 # m/s
max_rotation = 1.0 # rad/s
[camera]
device = "/dev/video0" # or "picam" for the Pi Camera Module
width = 640
height = 480
vision_model = "moondream" # "llava" | "moondream" | "none"
ollama_url = "http://localhost:11434"
[audio]
mic_device = "default"
speaker_device = "default"
whisper_model = "base" # "tiny" | "base" | "small"
whisper_path = "/usr/local/bin/whisper-cpp"
piper_path = "/usr/local/bin/piper"
piper_voice = "en_US-lessac-medium"
[sensors]
lidar_port = "/dev/ttyUSB0"
lidar_type = "mock" # "rplidar" | "ydlidar" | "ros2" | "mock"
motion_pins = [17, 27] # BCM, PIR sensors
ultrasonic_pins = [23, 24] # BCM (trigger, echo); omit to disable
[safety]
min_obstacle_distance = 0.3 # meters — robot will not move closer than this
slow_zone_multiplier = 3.0 # start slowing at 0.3 × 3.0 = 0.9 m
approach_speed_limit = 0.3 # fraction of max speed near obstacles
max_drive_duration = 30 # seconds — watchdog auto-stop window
sensor_timeout_secs = 5 # block movement if sensors go stale this long
blind_mode_speed_limit = 0.2 # speed cap when sensors are mock/unavailable
estop_pin = 4 # BCM — physical emergency-stop button
bump_sensor_pins = [5, 6] # BCM — chassis microswitches
bump_reverse_distance = 0.15 # meters to back up after a bump
predict_collisions = true # trajectory prediction (needs LIDAR)
confirm_movement = false # require verbal confirmation before each move

drive controls omni-directional movement. It maps a natural-language action to one of four motor backends and computes the move duration from the requested distance and your configured speed limits.

drive(action="forward", distance=1.0)
drive(action="rotate_left", distance=90)
drive(action="left", distance=0.5, speed=0.3)
drive(action="stop")
FieldTypeDefaultMeaning
actionstring— (required)forward, backward, left, right (strafe), rotate_left, rotate_right, stop, or custom.
distancenumber0.5 m / 90°Meters for linear moves, degrees for rotation.
speednumber0.5Speed multiplier 0.01.0, scaled by drive.max_speed.
linear_x / linear_y / angular_znumber0.0For action="custom": raw velocities (-1.0 to 1.0).

stop always executes immediately and is never blocked. Drive commands are rate-limited to one per second, and every move duration is clamped to safety.max_drive_duration so a single command can never run away.

Drive backends (drive.backend):

BackendBehaviour
ros2Publishes geometry_msgs/msg/Twist to the configured ros2_topic (default /cmd_vel) by shelling out to ros2 topic pub. For production, swap in rclrs (Rust ROS2 bindings).
serialSends M <lx> <ly> <az> <ms> frames to an Arduino/motor controller on drive.serial_port.
gpioDirect PWM via rppal (requires the gpio feature).
mockLogs the command and returns success. The default — use it for development.

sense reads the robot’s environment from LIDAR, PIR motion sensors, and an ultrasonic range finder.

sense(action="scan", direction="forward")
sense(action="clear_ahead")
sense(action="all")
FieldTypeMeaning
actionstring (required)scan (LIDAR), motion (PIR), distance (ultrasonic), clear_ahead (forward path check), or all (combined report).
directionstringFor scan: forward (default), left, right, back, or all.

LIDAR backend is set by sensors.lidar_type (rplidar, ydlidar, ros2, or mock). When real hardware is unavailable the kit falls back to a simulated room scan, so sense always returns something usable in mock mode.

look captures a camera frame (via ffmpeg, falling back to fswebcam) and optionally describes it with a local vision model through Ollama.

look(action="describe")
look(action="find", prompt="a red ball")
look(action="capture")
FieldTypeMeaning
actionstring (required)capture (photo only), describe (photo + AI description), or find (look for a specific thing — prompt required).
promptstringWhat to focus on for describe, or what to search for with find.

Set camera.vision_model = "none" to disable descriptions and capture only. Frames are saved under ~/.revka/captures/.

listen records from the microphone for a few seconds and transcribes it with Whisper.cpp.

listen(duration=5)
FieldTypeDefaultMeaning
durationinteger5Recording length in seconds (130).
promptstringOptional context hint for transcription, e.g. "The speaker is a child".

Requires the whisper.cpp binary at audio.whisper_path. Model size is set by audio.whisper_model (tiny fastest → small most accurate).

speak converts text to speech with Piper TTS and plays it through the speaker. It can also play short sound effects.

speak(text="Hello, I am ready.")
speak(sound="chime")
FieldTypeMeaning
textstringThe text to say.
emotionstringTone: neutral (default), excited, sad, or whisper.
soundstringPlay a sound effect instead of speaking (e.g. beep, chime, laugh, alert).

Requires the piper binary at audio.piper_path and a voice model in audio.piper_voice.

emote shows a face on an LED matrix and plays a matching sound, making the robot more engaging.

emote(expression="happy")
emote(expression="excited", animation="dance")
FieldTypeDefaultMeaning
expressionstring (required)happy, sad, surprised, thinking, sleepy, excited, love, angry, confused, or wink.
animationstringOptional: blink, nod, shake, or dance.
soundbooleantruePlay the matching sound effect.
durationinteger3Seconds to hold the expression (capped at 10).

The LED backend writes to a FIFO/control script if present, and logs the pattern harmlessly when no LED hardware is connected.

The SafetyMonitor is not a tool the LLM can call — it is an independent background task that gates every movement. The SafeDrive wrapper sits in front of DriveTool, so when you build tools with create_safe_tools(...), every drive call must be pre-cleared by the monitor before the motors turn.

It enforces five safety layers:

  1. Pre-move checks — verify the path is clear before any movement.
  2. Active monitoring — continuous sensor polling while moving; speed is scaled down by proximity.
  3. Reactive stops — instant halt on obstacle or bump-sensor contact.
  4. Watchdog timer — auto-stop if no new command arrives within max_drive_duration; movement is also blocked if sensor data goes stale.
  5. Hardware E-stop — a physical button on estop_pin overrides everything; it stays latched until explicitly reset.

Safety state and events are observable: the monitor tracks a can_move flag, an estop_active flag, the current minimum obstacle distance, a proximity-based speed multiplier, and a block reason. It broadcasts SafetyEvents — ObstacleDetected, EmergencyStop, WatchdogTimeout, BumpDetected, MovementApproved, MovementDenied, and Recovered — over a tokio::sync::broadcast channel you can subscribe to.

Config keyTypeDefaultMeaning
min_obstacle_distancefloat (m)0.3Robot will not move if an obstacle is closer than this.
slow_zone_multiplierfloat3.0Start slowing within min_obstacle_distance × multiplier.
approach_speed_limitfloat0.3Speed cap (fraction of max) inside the slow zone.
max_drive_durationint (s)30Watchdog window — auto-stop after this without a new command.
sensor_timeout_secsint (s)5Block all movement if sensor data is older than this.
blind_mode_speed_limitfloat0.2Speed cap when sensors are in mock/unavailable mode.
estop_pinint (BCM)4GPIO for the physical emergency-stop button (null to disable).
bump_sensor_pinsint[] (BCM)[5, 6]Chassis microswitches; trigger an immediate stop.
bump_reverse_distancefloat (m)0.15How far to back up after a bump.
predict_collisionsbooltrueTrajectory prediction (needs LIDAR).
confirm_movementboolfalseRequire verbal confirmation before each move.

Build the safety monitor first, then create the tool set wrapped with SafeDrive, and register the tools with your agent loop:

use revka_robot_kit::{RobotConfig, SafetyMonitor, create_safe_tools};
use std::sync::Arc;
// Load configuration (or RobotConfig::default() for mock mode)
let config = RobotConfig::load(std::path::Path::new("~/.revka/robot.toml"))?;
// Create the independent safety monitor
let (safety, _events_rx) = SafetyMonitor::new(config.safety.clone());
let safety = Arc::new(safety);
// All six tools, with drive wrapped by SafeDrive
let tools = create_safe_tools(&config, safety.clone());
// Register `tools` with the Revka agent loop, then drive the safety loop:
// tokio::spawn(async move { safety.run(sensor_rx).await; });

Use create_tools(&config) instead if you deliberately want the tools without the safety wrapper (mock development only). Once registered, the agent reaches the robot through natural language:

Terminal window
revka agent -m "Look around, tell me what you see, then move forward 1 meter if the path is clear"

For a full robot build — Pi 5, motor controller, LIDAR, camera, mic, speaker, LED matrix, E-stop, and bump sensors — follow crates/robot-kit/PI5_SETUP.md, which includes the GPIO wiring map used by the default robot.toml (E-stop on GPIO 4, bump sensors on 5/6, motor PWM on 12/13, PIR on 17/27, LED matrix on 18, ultrasonic on 23/24). Software dependencies (Ollama, Whisper.cpp, Piper, ALSA, ffmpeg) are listed in crates/robot-kit/README.md.