Hello World in Rust
In this tutorial, you'll build a simple temperature sensor application with a publisher and subscriber. By the end, you'll understand the core HDDS concepts.
Time: ~10 minutes Prerequisites: Rust installed
What We're Building
The publisher simulates a temperature sensor sending readings every second. The subscriber receives and displays them.
Step 1: Create a New Project
cargo new hdds-hello-world
cd hdds-hello-world
Step 2: Add Dependencies
Edit Cargo.toml:
[package]
name = "hdds-hello-world"
version = "0.1.0"
edition = "2021"
[dependencies]
hdds = "1.0"
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0"
Step 3: Define the Data Type
Create src/temperature.rs:
use hdds::prelude::*;
use serde::{Deserialize, Serialize};
/// Temperature reading from a sensor
#[derive(Debug, Clone, Serialize, Deserialize, Topic)]
pub struct Temperature {
/// Unique sensor identifier
#[key]
pub sensor_id: String,
/// Temperature in Celsius
pub value: f32,
/// Unix timestamp in milliseconds
pub timestamp: u64,
}
#[key] attributeThe #[key] attribute marks sensor_id as the instance key. This means:
- Each unique
sensor_idis tracked independently - DDS maintains separate history per sensor
- Subscribers can filter by sensor
Step 4: Create the Publisher
Create src/bin/publisher.rs:
use anyhow::Result;
use hdds::prelude::*;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
// Import our data type
mod temperature;
use temperature::Temperature;
fn main() -> Result<()> {
println!("Starting temperature publisher...");
// 1. Create a DomainParticipant on domain 0
let participant = DomainParticipant::new(0)?;
println!("Joined domain 0");
// 2. Create a Topic for Temperature data
let topic = participant.create_topic::<Temperature>("temperature/room1")?;
println!("Created topic: temperature/room1");
// 3. Create a Publisher with default QoS
let publisher = participant.create_publisher()?;
// 4. Create a DataWriter for the topic
let writer = publisher.create_writer(&topic)?;
println!("DataWriter created, waiting for subscribers...");
// 5. Wait for at least one subscriber
writer.wait_for_subscribers(1, Duration::from_secs(30))?;
println!("Subscriber connected!");
// 6. Publish temperature readings
let sensor_id = "sensor-001".to_string();
for i in 0..10 {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
// Simulate temperature with some variation
let temperature = Temperature {
sensor_id: sensor_id.clone(),
value: 22.0 + (i as f32 * 0.5),
timestamp,
};
// Write the sample
writer.write(&temperature)?;
println!("Published: {:?}", temperature);
std::thread::sleep(Duration::from_secs(1));
}
println!("Publisher finished");
Ok(())
}
Step 5: Create the Subscriber
Create src/bin/subscriber.rs:
use anyhow::Result;
use hdds::prelude::*;
use std::time::Duration;
// Import our data type
mod temperature;
use temperature::Temperature;
fn main() -> Result<()> {
println!("Starting temperature subscriber...");
// 1. Create a DomainParticipant on domain 0
let participant = DomainParticipant::new(0)?;
println!("Joined domain 0");
// 2. Create a Topic for Temperature data
let topic = participant.create_topic::<Temperature>("temperature/room1")?;
println!("Created topic: temperature/room1");
// 3. Create a Subscriber with default QoS
let subscriber = participant.create_subscriber()?;
// 4. Create a DataReader for the topic
let reader = subscriber.create_reader(&topic)?;
println!("DataReader created, waiting for data...");
// 5. Read samples in a loop
loop {
// Wait for data with timeout
match reader.wait_for_data(Duration::from_secs(5)) {
Ok(()) => {
// Take all available samples
while let Some(sample) = reader.take()? {
println!(
"Received: sensor={}, temp={:.1}°C, time={}",
sample.sensor_id, sample.value, sample.timestamp
);
}
}
Err(e) if e.is_timeout() => {
println!("No data received in 5 seconds, waiting...");
}
Err(e) => return Err(e.into()),
}
}
}
Step 6: Update Cargo.toml for Binaries
Add the binary targets to Cargo.toml:
[[bin]]
name = "publisher"
path = "src/bin/publisher.rs"
[[bin]]
name = "subscriber"
path = "src/bin/subscriber.rs"
And create a shared module in src/bin/temperature.rs:
// src/bin/temperature.rs
pub use crate::temperature::Temperature;
Or, for simpler sharing, put the type in src/lib.rs:
// src/lib.rs
use hdds::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Topic)]
pub struct Temperature {
#[key]
pub sensor_id: String,
pub value: f32,
pub timestamp: u64,
}
Then update the binaries to use:
use hdds_hello_world::Temperature;
Step 7: Build and Run
Open two terminals:
Terminal 1 - Start the Subscriber:
cargo run --bin subscriber
Terminal 2 - Start the Publisher:
cargo run --bin publisher
Expected Output
Subscriber:
Starting temperature subscriber...
Joined domain 0
Created topic: temperature/room1
DataReader created, waiting for data...
Received: sensor=sensor-001, temp=22.0°C, time=1703001234567
Received: sensor=sensor-001, temp=22.5°C, time=1703001235567
Received: sensor=sensor-001, temp=23.0°C, time=1703001236567
...
Publisher:
Starting temperature publisher...
Joined domain 0
Created topic: temperature/room1
DataWriter created, waiting for subscribers...
Subscriber connected!
Published: Temperature { sensor_id: "sensor-001", value: 22.0, timestamp: 1703001234567 }
Published: Temperature { sensor_id: "sensor-001", value: 22.5, timestamp: 1703001235567 }
...
Publisher finished
Understanding the Code
DomainParticipant
let participant = DomainParticipant::new(0)?;
The DomainParticipant is your entry point to DDS. The 0 is the domain ID—participants must use the same domain to communicate.
Topic
let topic = participant.create_topic::<Temperature>("temperature/room1")?;
A Topic defines:
- Name:
"temperature/room1"- used for matching publishers and subscribers - Type:
Temperature- the data structure
Publisher and DataWriter
let publisher = participant.create_publisher()?;
let writer = publisher.create_writer(&topic)?;
writer.write(&temperature)?;
The Publisher groups DataWriters with shared properties. The DataWriter actually sends data.
Subscriber and DataReader
let subscriber = participant.create_subscriber()?;
let reader = subscriber.create_reader(&topic)?;
let sample = reader.take()?;
The Subscriber groups DataReaders. The DataReader receives data and removes it from the queue (take()) or peeks at it (read()).
Next Steps
Add Reliability
Make data delivery guaranteed:
let qos = DataWriterQos::default()
.reliability(Reliability::Reliable {
max_blocking_time: Duration::from_secs(1),
});
let writer = publisher.create_writer_with_qos(&topic, qos)?;
Add History
Keep the last N samples for late-joining subscribers:
let qos = TopicQos::default()
.durability(Durability::TransientLocal)
.history(History::KeepLast { depth: 10 });
let topic = participant.create_topic_with_qos::<Temperature>("temperature/room1", qos)?;
Add Multiple Sensors
for sensor_id in ["sensor-001", "sensor-002", "sensor-003"] {
let temp = Temperature {
sensor_id: sensor_id.to_string(),
value: 22.0 + rand::random::<f32>() * 5.0,
timestamp: now(),
};
writer.write(&temp)?;
}
Complete Source Code
The complete example is available on GitHub:
git clone https://github.com/hdds/hdds-examples.git
cd hdds-examples/hello-world-rust
cargo run --bin subscriber &
cargo run --bin publisher
What's Next?
- Hello World C - Same example in C
- QoS Policies - Fine-tune data distribution
- Examples - More complex examples