Skip to main content

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,
}
The #[key] attribute

The #[key] attribute marks sensor_id as the instance key. This means:

  • Each unique sensor_id is 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?