Thank you for your interest in contributing! This document provides guidelines for contributing code, documentation, and examples to this learning-focused portfolio project.
- Code of Conduct
- Getting Started
- Code Style Guidelines
- Adding New Nodes
- Testing Requirements
- Documentation Standards
- Pull Request Process
This project aims to be welcoming, inclusive, and educational. We expect all contributors to:
- Be respectful and constructive in discussions
- Focus on learning and teaching others
- Provide clear explanations for design choices
- Help newcomers understand Rust + ROS2 patterns
-
Complete the setup in docs/setup.md
-
Verify you can build and test:
source scripts/dev_env.sh cargo build --workspace cargo test
-
Read the architecture docs: docs/architecture.md
- Check open issues for tasks labeled
good-first-issueorhelp-wanted - Review the backlog.txt for planned features
- Propose new ideas by opening an issue first for discussion
Always run cargo fmt before committing:
cargo fmt --allCheck formatting in CI:
cargo fmt --all -- --checkFix all clippy warnings before submitting:
cargo clippy --workspace --all-targets -- -D warningsException: If a clippy lint is incorrect, use #[allow(clippy::lint_name)] with a comment explaining why.
Use pre-commit hooks to catch formatting and lint issues early:
# One-time setup
pipx install pre-commit
pre-commit install
# Hooks will now run automatically on git commitThis runs cargo fmt and cargo clippy before each commit, preventing CI failures. See docs/setup.md for details.
- Crates:
snake_case(e.g.,safety_watchdog,sensor_filter) - Types:
PascalCase(e.g.,WatchdogNode,FilterConfig) - Functions:
snake_case(e.g.,create_node,clamp_scan) - Constants:
SCREAMING_SNAKE_CASE(e.g.,DEFAULT_TIMEOUT_MS)
Use anyhow::Result for application code:
use anyhow::{Context, Result};
fn run() -> Result<()> {
let node = create_node().context("Failed to create node")?;
// ...
Ok(())
}Never use unwrap() or expect() in production code (examples are OK).
Use structured logging with tracing (never println!):
use rust_ros2_microstack::logging;
use tracing::{info, warn, error, debug};
fn main() -> anyhow::Result<()> {
// Initialize logging (supports JSON, OTLP, and more)
logging::init();
info!("Node started");
debug!(topic = %topic_name, "Subscribed to topic");
warn!(timeout_ms = %timeout, "Heartbeat timeout");
error!(error = %e, "Failed to publish");
Ok(())
}Log Levels:
error: Unrecoverable failureswarn: Recoverable issues (e.g., timeout detected)info: Major state changes (e.g., node started)debug: Detailed execution flowtrace: Very verbose (rarely needed)
For comprehensive documentation, see docs/logging.md which covers:
- Configuration via environment variables
- Output formats (console, JSON, OpenTelemetry)
- Distributed tracing with spans
- Best practices and examples
Nodes should demonstrate a distinct ROS2 pattern or solve a real robotics problem. Before adding:
- Check for existing nodes that could be extended
- Open an issue describing the node's purpose and learning objectives
- Get feedback from maintainers on design
Binary crate (apps/) if:
- Meant to be run directly by end users
- Has a CLI interface
- Example:
teleop_mux
Library crate (nodes/) if:
- Reusable logic that can be tested independently
- May be used by multiple binaries
- Example:
safety_watchdog,sensor_filter
Uncertain? Default to library crate (can always add a thin binary wrapper later).
-
Create the crate:
# For library node cargo new --lib nodes/my_node # For binary app cargo new apps/my_app
-
Update workspace
Cargo.toml:[workspace] members = [ # ... existing members "nodes/my_node", ]
-
Update node's
Cargo.toml:[package] name = "my_node" version = "0.1.0" edition = "2024" [dependencies] r2r = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] }
-
Add module-level documentation:
//! # My Node //! //! Brief description of what this node does. //! //! ## Learning Objectives //! - Pattern 1 //! - Pattern 2 //! //! ## Usage //! ```bash //! cargo run -p my_node -- --help //! ```
-
Implement with clear comments:
/// Creates the node and configures subscriptions/publishers. /// /// # Arguments /// - `ctx`: r2r context /// - `config`: Node configuration /// /// # Returns /// Configured node or error pub fn create_node(ctx: r2r::Context, config: Config) -> anyhow::Result<r2r::Node> { // Implementation with inline comments explaining r2r patterns }
-
Add unit tests:
#[cfg(test)] mod tests { use super::*; #[test] fn test_node_creation() { // Test logic } }
-
Update
docs/nodes.mdwith implementation deep dive -
Update README's topic table with new subscriptions/publications
-
Add to backlog or create GitHub issue for integration tests
- Module-level documentation with learning objectives
- Inline doc comments explaining r2r patterns
- Unit tests covering core logic
- CLI interface with
--helptext - Updated
docs/nodes.md - Updated README topic table
- Passes
cargo fmtandcargo clippy - Integration test or manual test plan
Location: src/lib.rs or src/tests/ in each crate
Coverage: Core logic, message transformations, configuration parsing
Example:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clamp_ranges() {
let result = clamp(5.0, 0.0, 10.0);
assert_eq!(result, 5.0);
}
#[test]
fn test_clamp_min() {
let result = clamp(-1.0, 0.0, 10.0);
assert_eq!(result, 0.0);
}
}Run: cargo test
Location: tests/integration/
Purpose: Test actual ROS2 message flow between nodes
Example:
// tests/integration/test_my_node.rs
use r2r::{Context, Node};
use tokio::time::{timeout, Duration};
#[tokio::test]
async fn test_my_node_publishes() {
let ctx = Context::create().unwrap();
let mut node = Node::create(ctx, "test_node", "").unwrap();
// Test logic with actual r2r pub/sub
// ...
}Run: cargo test --test '*'
Note: Integration tests may require ROS2 environment (run after source scripts/dev_env.sh).
Location: benches/ in crate root
When required: For performance-critical operations (e.g., sensor processing)
Example:
// benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn benchmark_operation(c: &mut Criterion) {
c.bench_function("operation", |b| {
b.iter(|| my_operation(black_box(input)))
});
}
criterion_group!(benches, benchmark_operation);
criterion_main!(benches);Run: cargo bench
All public items must have doc comments:
/// Represents a safety watchdog node.
///
/// Monitors heartbeat messages and publishes zero-velocity commands
/// when heartbeats stop arriving within the configured timeout.
///
/// # Examples
/// ```no_run
/// let node = WatchdogNode::new(config)?;
/// node.run().await?;
/// ```
pub struct WatchdogNode {
// ...
}Explain "why" not just "what":
// ❌ Bad: States the obvious
/// Sets the timeout.
pub fn set_timeout(&mut self, timeout: Duration) { ... }
// ✅ Good: Explains rationale
/// Sets the timeout for heartbeat monitoring.
///
/// After this duration without heartbeats, the node publishes
/// zero-velocity commands to safely stop the robot.
pub fn set_timeout(&mut self, timeout: Duration) { ... }Explain r2r patterns for learners:
// Create a subscriber with best-effort QoS.
// Best-effort is used here because heartbeats are high-rate;
// missing one is acceptable since the timeout will trigger anyway.
let subscriber = node.create_subscription::<Empty>(
"/heartbeat",
QosProfile::best_effort()
)?;When adding significant features, update:
- docs/architecture.md - Design decisions
- docs/nodes.md - Implementation patterns
- README.md - User-facing overview
Standalone examples should:
- Be < 100 lines of code
- Have extensive inline comments
- Include a "What you'll learn" comment at the top
- Be runnable with
cargo run --example <name>
-
Branch from
main:git checkout -b feature/my-feature
-
Write clear commit messages:
Add sensor_filter node with LaserScan clamping - Implements configurable min/max range clamping - Adds benchmarks showing <1ms processing time - Updates docs/nodes.md with implementation details Closes #42 -
Run quality checks:
cargo fmt --all cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace cargo doc --no-deps --workspace -
Update documentation (if applicable):
- README.md
- docs/nodes.md
- Inline doc comments
- backlog.txt or GitHub issues
-
Push your branch:
git push origin feature/my-feature
-
Open a Pull Request with:
- Clear title: "Add sensor_filter node"
- Description: What changes, why, learning objectives
- Linked issues: "Closes #42"
- Checklist: Use the PR template (see
.github/PULL_REQUEST_TEMPLATE.md)
-
Respond to feedback promptly and respectfully
- Code follows style guidelines (
cargo fmt,cargo clippy) - Unit tests added for new logic
- Integration tests added (if applicable)
- Documentation updated (README, docs/, inline comments)
- Learning objectives clearly explained
- All CI checks pass
- Manual testing performed and described
- Automated checks run (CI: build, fmt, clippy)
- Maintainer review (usually within 3 days)
- Requested changes (if any)
- Approval → Merge
- General questions: Open a GitHub Discussion
- Bug reports: Open an Issue
- Feature proposals: Open an Issue for discussion first
Thank you for contributing to Rust ROS2 learning! Your work helps others learn ROS2 in Rust. 🦀🤖