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).
Architecture
Section titled “Architecture”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 · LEDsThe 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 & feature flags
Section titled “Build & feature flags”Build the crate as a workspace package:
cargo build -p revka-robot-kit --release| Feature | Default | What it enables |
|---|---|---|
safety | on | The SafetyMonitor background task and the SafeDrive wrapper. Strongly recommended; keep it enabled. |
gpio | off | Direct GPIO control via rppal (Raspberry Pi / Linux only). |
ros2 | off | ROS2 integration build flag. |
lidar | off | LIDAR support build flag. |
vision | off | Camera + vision-model build flag. |
Configuration (robot.toml)
Section titled “Configuration (robot.toml)”All hardware and limits live in robot.toml. Copy the template from the crate and edit it for your robot:
mkdir -p ~/.revkacp 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/smax_rotation = 1.0 # rad/s
[camera]device = "/dev/video0" # or "picam" for the Pi Camera Modulewidth = 640height = 480vision_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 sensorsultrasonic_pins = [23, 24] # BCM (trigger, echo); omit to disable
[safety]min_obstacle_distance = 0.3 # meters — robot will not move closer than thisslow_zone_multiplier = 3.0 # currently inert — slow zone is hardcoded at min_obstacle_distance × 3.0approach_speed_limit = 0.3 # currently inert — slow-zone speed is a linear ramp, this cap is not appliedmax_drive_duration = 30 # seconds — cap per move; watchdog emits WatchdogTimeout but does not auto-stopsensor_timeout_secs = 5 # currently inert — stale-sensor block is hardcoded at 5 sblind_mode_speed_limit = 0.2 # currently inert — no blind-mode speed-capping logic existsestop_pin = 4 # BCM — physical emergency-stop buttonbump_sensor_pins = [5, 6] # BCM — chassis microswitchesbump_reverse_distance = 0.15 # currently inert — bump stops movement and auto-recovers after ~2s, no reversepredict_collisions = true # currently inert — no trajectory-prediction logic is implementedconfirm_movement = false # currently inert — no verbal-confirmation logic existsThe tools
Section titled “The tools”drive — movement
Section titled “drive — movement”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")| Field | Type | Default | Meaning |
|---|---|---|---|
action | string | — (required) | forward, backward, left, right (strafe), rotate_left, rotate_right, stop, or custom. |
distance | number | 0.5 m / 90° | Meters for linear moves, degrees for rotation. |
speed | number | 0.5 | Speed multiplier 0.0–1.0, scaled by drive.max_speed. |
linear_x / linear_y / angular_z | number | 0.0 | For action="custom": raw velocities (-1.0 to 1.0). |
Drive commands are rate-limited to one per second. stop is never blocked by the SafetyMonitor/SafeDrive layer, but it is still subject to DriveTool’s 1 s rate limit in the unwrapped tool. Every move duration is clamped to safety.max_drive_duration so a single command can never run away.
Drive backends (drive.backend):
| Backend | Behaviour |
|---|---|
ros2 | Publishes 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). |
serial | Sends M <lx> <ly> <az> <ms> frames to an Arduino/motor controller on drive.serial_port. |
gpio | Direct PWM via rppal (requires the gpio feature). |
mock | Logs the command and returns success. The default — use it for development. |
sense — perception
Section titled “sense — perception”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")| Field | Type | Meaning |
|---|---|---|
action | string (required) | scan (LIDAR), motion (PIR), distance (ultrasonic), clear_ahead (forward path check), or all (combined report). |
direction | string | For 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 — vision
Section titled “look — vision”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")| Field | Type | Meaning |
|---|---|---|
action | string (required) | capture (photo only), describe (photo + AI description), or find (look for a specific thing — prompt required). |
prompt | string | What 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 — hearing
Section titled “listen — hearing”listen records from the microphone for a few seconds and transcribes it with Whisper.cpp.
listen(duration=5)| Field | Type | Default | Meaning |
|---|---|---|---|
duration | integer | 5 | Recording length in seconds (1–30). |
prompt | string | — | Optional 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 — voice
Section titled “speak — voice”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")| Field | Type | Meaning |
|---|---|---|
text | string | The text to say. |
emotion | string | Tone: neutral (default), excited, sad, or whisper. |
sound | string | Play 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 — expression
Section titled “emote — expression”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")| Field | Type | Default | Meaning |
|---|---|---|---|
expression | string (required) | — | happy, sad, surprised, thinking, sleepy, excited, love, angry, confused, or wink. |
animation | string | — | Optional: blink, nod, shake, or dance. |
sound | boolean | true | Play the matching sound effect. |
duration | integer | 3 | Seconds 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 safety monitor
Section titled “The safety monitor”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:
- Pre-move checks — verify the path is clear before any movement.
- Active monitoring — continuous sensor polling while moving; speed is scaled down by proximity.
- Reactive stops — instant halt on obstacle or bump-sensor contact.
- Watchdog timer — broadcasts a
WatchdogTimeoutevent when no new command arrives withinmax_drive_durationbut does not auto-stop the robot; runaway is prevented instead by clamping each individual move’s duration tosafety.max_drive_durationinDriveTool. Movement is also blocked if sensor data goes stale (hardcoded at 5 s). - Hardware E-stop — a physical button on
estop_pinoverrides 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 key | Type | Default | Meaning |
|---|---|---|---|
min_obstacle_distance | float (m) | 0.3 | Robot will not move if an obstacle is closer than this. |
slow_zone_multiplier | float | 3.0 | Currently inert — the slow zone is hardcoded at min_obstacle_distance × 3.0 regardless of this value. Changing it has no effect. |
approach_speed_limit | float | 0.3 | Currently inert — the slow-zone speed is a linear ramp from 0 to 1 and this cap is never applied. |
max_drive_duration | int (s) | 30 | Cap applied to each individual move’s duration in DriveTool; the watchdog fires a WatchdogTimeout event after this interval but does not auto-stop the robot. |
sensor_timeout_secs | int (s) | 5 | Currently inert — the stale-sensor block is hardcoded at 5 s. Changing this value has no effect. |
blind_mode_speed_limit | float | 0.2 | Currently inert — no blind-mode speed-capping logic exists. Setting this value has no effect. |
estop_pin | int (BCM) | 4 | GPIO for the physical emergency-stop button (null to disable). |
bump_sensor_pins | int[] (BCM) | [5, 6] | Chassis microswitches; trigger an immediate stop. |
bump_reverse_distance | float (m) | 0.15 | Currently inert — on a bump the monitor stops movement and auto-recovers after ~2 s without backing up. |
predict_collisions | bool | true | Placeholder — no trajectory-prediction logic is implemented. Setting this value has no effect. |
confirm_movement | bool | false | Currently inert — no confirmation logic exists. Setting this true has no effect. |
Wiring it into an agent
Section titled “Wiring it into an agent”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 monitorlet (safety, _events_rx) = SafetyMonitor::new(config.safety.clone());let safety = Arc::new(safety);
// All six tools, with drive wrapped by SafeDrivelet 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:
revka agent -m "Look around, tell me what you see, then move forward 1 meter if the path is clear"Raspberry Pi deployment
Section titled “Raspberry Pi deployment”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.