Reviews Rust firmware code for the BVR (Base Vectoring Rover) with focus on safety-critical systems, CAN bus protocol compliance, motor control logic, state machine correctness, and embedded testing patterns. Use when reviewing BVR firmware changes, debugging actuator control, testing motor communication, validating safety mechanisms, checking async patterns, or evaluating control system modifications. Covers watchdog implementation, e-stop handling, rate limiting, VESC motor controller integration, and Tokio async runtime patterns.
This skill provides comprehensive code review for the BVR (Base Vectoring Rover) firmware, focusing on safety-critical systems, real-time control, and CAN bus communication.
The BVR firmware runs on a Jetson Orin NX and controls a four-wheel differential drive rover using VESC motor controllers over CAN bus. The system is safety-critical with multiple layers of protection including watchdogs, e-stop mechanisms, and rate limiting.
Key Architecture:
bvr/firmware/bins/bvrd/src/main.rs)Critical Files:
crates/state/src/lib.rs (130 lines)crates/control/src/lib.rs (243 lines)crates/can/src/lib.rscrates/can/src/vesc.rscrates/can/src/leds.rsbins/bvrd/src/main.rs (1191 lines)config/bvr.toml (152 lines)Purpose: Detect communication timeouts and transition to safe state (Idle) when commands stop arriving.
Location: bvr/firmware/crates/control/src/lib.rs:162-193
Key Points to Review:
feed() called on every valid command receptionis_timed_out() checked in main control loopEvent::CommandTimeout in state machineExample Pattern:
// Good: Watchdog properly integrated
let mut watchdog = Watchdog::new(Duration::from_millis(500));
loop {
if let Some(cmd) = recv_command() {
watchdog.feed(); // Reset timer on command
}
if watchdog.is_timed_out() {
state_machine.handle(Event::CommandTimeout);
// Motors automatically stop in Idle mode
}
}
Red Flags:
feed() called unconditionally (bypasses timeout detection)See: safety-checklist.md for detailed verification steps.
Purpose: Immediate safety override from any operational mode, requiring explicit release to resume.
Location: bvr/firmware/crates/state/src/lib.rs:7-130
State Machine Modes:
Disabled - Initial state, motors offIdle - Ready state, motors enabled but no movementTeleop - Human control via gamepad/keyboardAutonomous - Autonomous navigationEStop - Emergency stop, requires releaseFault - Error state, requires fault clearValid E-Stop Transitions:
EStop (via Event::EStop)EStop → Idle (via Event::EStopRelease only)Key Points to Review:
Event::EStop handled from all active modes (Idle, Teleop, Autonomous)Event::EStopReleaseforce_estop() method available for critical overridesExample Pattern:
// Good: Proper e-stop handling
match (self.mode, event) {
(_, Event::EStop) => {
info!(?self.mode, "E-stop triggered");
self.mode = Mode::EStop;
// Motors automatically stopped by is_driving() check
}
(Mode::EStop, Event::EStopRelease) => {
info!("E-stop released, transitioning to Idle");
self.mode = Mode::Idle;
}
(Mode::EStop, Event::Enable) => {
warn!("Cannot enable directly from e-stop, must release first");
// No state change - requires explicit release
}
// ...
}
Red Flags:
See: state-machine.md for complete transition diagram.
Purpose: Progressive acceleration/deceleration to prevent wheel slip, mechanical stress, and loss of control.
Location: bvr/firmware/crates/control/src/lib.rs:101-160
Rate Limiter Configuration:
Key Points to Review:
Example Pattern:
// Good: Rate limiting with direction change detection
impl RateLimiter {
pub fn limit(&mut self, target: Twist, dt: f32) -> Twist {
// Detect direction change (requires deceleration)
let accel = if self.prev.linear * target.linear < 0.0 {
self.max_decel // Braking
} else {
self.max_accel // Accelerating
};
// Apply rate limit
let delta = target.linear - self.prev.linear;
let max_delta = accel * dt;
let limited = self.prev.linear + delta.clamp(-max_delta, max_delta);
// Clamp to absolute limits
Twist {
linear: limited.clamp(-self.max_linear, self.max_linear),
angular: /* similar for angular */,
boost: target.boost,
}
}
}
Red Flags:
See: safety-checklist.md for validation tests.
Purpose: Enforce valid operational mode transitions and prevent unsafe state combinations.
Key Points to Review:
is_driving() check used to gate motor commandsis_safe() check used for configuration changesCritical Checks:
// is_driving() - only Teleop and Autonomous should drive
pub fn is_driving(&self) -> bool {
matches!(self.mode, Mode::Teleop | Mode::Autonomous)
}
// is_safe() - only allow config changes in safe modes
pub fn is_safe(&self) -> bool {
matches!(self.mode, Mode::Disabled | Mode::Idle | Mode::EStop)
}
Example Pattern:
// Good: Explicit transition handling with logging
match (self.mode, event) {
(Mode::Idle, Event::Enable) => {
self.mode = Mode::Teleop;
info!(old = ?Mode::Idle, new = ?Mode::Teleop, "Mode transition");
}
(Mode::Disabled, Event::Enable) => {
warn!("Cannot enable from Disabled, transition to Idle first");
// Reject transition
}
_ => {
debug!(?self.mode, ?event, "Unhandled event");
}
}
See: state-machine.md for full transition table and unit tests.
Purpose: Send velocity/duty commands to 4 VESC motor controllers (FL, FR, RL, RR) and receive status.
Location: bvr/firmware/crates/can/src/vesc.rs
VESC CAN IDs: 0, 1, 2, 3 (configured in bvr.toml)
Command Frame Format (Extended CAN):
(command_id << 8) | vesc_idCommand Types:
SetDuty (0): Duty cycle control, -1.0 to 1.0 scaled to ±100,000SetRpm (3): Electrical RPM (divide by pole pairs for mechanical)SetCurrent (1): Current control in milliampsKey Points to Review:
data.len() >= 8)Example Pattern:
// Good: VESC command with proper encoding
pub fn set_duty(&mut self, duty: f32) -> Result<Frame, CanError> {
let clamped_duty = duty.clamp(-1.0, 1.0);
let duty_value = (clamped_duty * 100_000.0) as i32;
let frame_id = (CMD_SET_DUTY << 8) | self.id; // Extended ID
let data = duty_value.to_be_bytes(); // Big-endian
Frame::new(Id::Extended(frame_id), &data)
}
// Good: Safe status parsing with bounds check
fn parse_status1(&mut self, data: &[u8]) {
if data.len() < 8 {
warn!("STATUS1 frame too short");
return;
}
self.state.status.erpm = i32::from_be_bytes([data[0], data[1], data[2], data[3]]);
self.state.status.current = i16::from_be_bytes([data[4], data[5]]) as f32 / 10.0;
self.state.status.duty = i16::from_be_bytes([data[6], data[7]]) as f32 / 1000.0;
}
Red Flags:
See: can-protocol.md for complete VESC protocol reference.
Purpose: Send LED mode commands to RP2350/ESP32-S3 MCU for rover status indication.
Location: bvr/firmware/crates/can/src/leds.rs
LED CAN ID Range: 0x0B00-0x0BFF (extended IDs for peripherals)
LED Modes:
StateLinked (0x10): MCU automatically sets color based on rover modeSolid: Fixed colorPulse: Breathing effect (configurable period)Flash: Strobe effect (configurable period)Chase: Moving patternRover State Colors:
Key Points to Review:
Example Pattern:
// Good: State-linked LED control
pub fn update_leds(&mut self, mode: Mode) {
let led_mode = match mode {
Mode::Idle => LedMode::idle(), // Blue solid
Mode::Teleop => LedMode::teleop(), // Green pulse 2s
Mode::Autonomous => LedMode::autonomous(), // Cyan pulse 1.5s
Mode::EStop => LedMode::estop(), // Red flash 200ms
Mode::Fault => LedMode::fault(), // Orange flash 500ms
_ => LedMode::off(),
};
self.send_led_command(led_mode)?;
}
See: can-protocol.md for LED protocol details.
Key Points to Review:
Result types with CanErrorExample Pattern:
// Good: Comprehensive error handling
#[derive(Error, Debug)]
pub enum CanError {
#[error("Socket error: {0}")]
Socket(String),
#[error("Invalid CAN ID: {0}")]
InvalidId(u32),
#[error("Timeout waiting for response")]
Timeout,
#[error("Invalid frame data")]
InvalidFrame,
}
// Good: Non-blocking receive with timeout
match self.socket.read_frame_timeout(Duration::from_millis(10)) {
Ok(frame) => { /* process */ }
Err(e) if e.kind() == ErrorKind::WouldBlock => { /* no data */ }
Err(e) => {
error!(?e, "CAN socket read error");
return Err(CanError::Socket(e.to_string()));
}
}
Convention: Use thiserror for library crates, anyhow for binary crates.
Key Points to Review:
thiserror#[error("...")]#[from] attribute used for error conversionanyhow::Result and .context()?e (Debug) not %e (Display)Example Pattern:
// Library crate (crates/teleop/src/lib.rs)
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TeleopError {
#[error("Network error: {0}")]
Network(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Connection timeout")]
Timeout,
}
// Binary crate (bins/bvrd/src/main.rs)
use anyhow::{Context, Result};
fn load_config() -> Result<Config> {
let contents = fs::read_to_string("bvr.toml")
.context("Failed to read bvr.toml")?;
toml::from_str(&contents)
.context("Failed to parse bvr.toml")
}
Red Flags:
Result<T, String>)unwrap(), expect() in libraries)?e)Runtime: Multi-threaded Tokio (#[tokio::main])
Key Points to Review:
#[tokio::main] on main functiontokio::spawn for concurrent subsystemsExample Pattern:
// Good: Async main with spawned tasks
#[tokio::main]
async fn main() -> Result<()> {
let (cmd_tx, cmd_rx) = mpsc::channel::<Command>(100);
let (telem_tx, telem_rx) = watch::channel(Telemetry::default());
// Spawn teleop server task
tokio::spawn(async move {
if let Err(e) = teleop_server.run(cmd_tx, telem_rx).await {
error!(?e, "Teleop server error");
}
});
// Spawn metrics task
tokio::spawn(async move {
if let Err(e) = metrics.run(telem_rx).await {
error!(?e, "Metrics task error");
}
});
// Main control loop
loop {
tokio::select! {
Some(cmd) = cmd_rx.recv() => { /* handle */ }
_ = tokio::signal::ctrl_c() => {
info!("Shutdown signal received");
break;
}
}
}
Ok(())
}
Red Flags:
Convention: Module-level //! docs at top of lib.rs files, function-level /// docs.
Key Points to Review:
//! module-level docs describe crate purpose/// function docs include examples for public API///Example Pattern:
//! State machine and mode management for bvr.
//!
//! This crate implements the rover's operational modes and state
//! transitions, including safety mechanisms like e-stop handling.
/// Represents the rover's current operational mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
/// Motors disabled, awaiting initialization
Disabled,
/// Ready state, motors enabled but stationary
Idle,
/// Under human control via teleop
Teleop,
// ...
}
/// Handles a state machine event and transitions to a new mode if valid.
///
/// # Example
/// ```
/// let mut sm = StateMachine::new();
/// sm.handle(Event::Enable);
/// assert_eq!(sm.mode(), Mode::Idle);
/// ```
pub fn handle(&mut self, event: Event) {
// ...
}
Convention: Use version.workspace = true for shared dependencies.
Key Points to Review:
Cargo.tomlCargo.toml uses version.workspace = trueExample Pattern:
# Root bvr/firmware/Cargo.toml
[workspace.dependencies]
tokio = { version = "1.41", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0"
# Crate Cargo.toml
[dependencies]
tokio = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
Convention: Tests in #[cfg(test)] mod tests block at bottom of file.
Key Points to Review:
test_watchdog_timeout_triggers)assert! with helpful messagesExample Pattern:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_watchdog_timeout_triggers() {
let mut wd = Watchdog::new(Duration::from_millis(100));
assert!(wd.is_timed_out(), "Watchdog should timeout initially");
wd.feed();
assert!(!wd.is_timed_out(), "Watchdog should not timeout after feed");
std::thread::sleep(Duration::from_millis(150));
assert!(wd.is_timed_out(), "Watchdog should timeout after duration");
}
#[test]
fn test_estop_requires_explicit_release() {
let mut sm = StateMachine::new();
sm.handle(Event::Enable);
assert_eq!(sm.mode(), Mode::Teleop);
sm.handle(Event::EStop);
assert_eq!(sm.mode(), Mode::EStop);
sm.handle(Event::Enable);
assert_eq!(sm.mode(), Mode::EStop, "Cannot enable directly from e-stop");
sm.handle(Event::EStopRelease);
assert_eq!(sm.mode(), Mode::Idle, "Release transitions to Idle");
}
}
Key Points to Review:
See: Run tests with cargo test (native) or cargo test --target aarch64-unknown-linux-gnu (cross).
For more detailed information, see:
# Run all tests
cargo test
# Check compilation without building
cargo check
# Build for Jetson (requires cross)
cargo build --release --target aarch64-unknown-linux-gnu
# Deploy to rover
./deploy.sh <rover-hostname>