| Crates.io | coapum |
| lib.rs | coapum |
| version | 0.2.0 |
| created_at | 2025-08-19 19:20:51.191371+00 |
| updated_at | 2025-08-19 19:20:51.191371+00 |
| description | A modern, ergonomic CoAP (Constrained Application Protocol) library for Rust with support for DTLS, observers, and asynchronous handlers |
| homepage | https://github.com/jaredwolff/coapum |
| repository | https://github.com/jaredwolff/coapum |
| max_upload_size | |
| id | 1802327 |
| size | 583,212 |
A modern, ergonomic CoAP (Constrained Application Protocol) library for Rust with support for DTLS, observers, and asynchronous handlers.
Add Coapum to your Cargo.toml:
[dependencies]
coapum = "0.2.0"
# For standalone SenML usage
coapum-senml = "0.1.0"
use coapum::{
router::RouterBuilder,
observer::memory::MemObserver,
serve,
extract::{Json, Path, StatusCode},
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct DeviceState {
temperature: f32,
humidity: f32,
}
// Handler with automatic JSON deserialization and path parameter extraction
async fn update_device(
Path(device_id): Path<String>,
Json(state): Json<DeviceState>,
) -> Result<StatusCode, StatusCode> {
println!("Updating device {}: temp={}ยฐC", device_id, state.temperature);
Ok(StatusCode::Changed)
}
// Observer handler for device state notifications
async fn get_device_state(Path(device_id): Path<String>) -> Json<DeviceState> {
Json(DeviceState {
temperature: 23.5,
humidity: 45.2,
})
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create router with ergonomic builder API
let router = RouterBuilder::new((), MemObserver::new())
.post("/device/:id", update_device)
.get("/device/:id", get_device_state)
.observe("/device/:id", get_device_state, get_device_state)
.build();
// Start server
serve::serve("127.0.0.1:5683".to_string(), Default::default(), router).await?;
Ok(())
}
use coapum::{
dtls::{
cipher_suite::CipherSuiteId,
config::{Config, ExtendedMasterSecretType},
},
config,
router::RouterBuilder,
observer::sled::SledObserver,
serve,
};
use std::{collections::HashMap, sync::{Arc, RwLock}};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup PSK store
let psk_store = Arc::new(RwLock::new(HashMap::new()));
psk_store.write().unwrap().insert(
"device123".to_string(),
"secret_key".as_bytes().to_vec()
);
// Create router with persistent observer storage
let router = RouterBuilder::new((), SledObserver::new("observer.db").unwrap())
.get("/status", || async { "OK" })
.build();
// Configure DTLS
let dtls_config = Config {
psk: Some(Arc::new(move |hint: &[u8]| {
let hint = String::from_utf8_lossy(hint);
psk_store.read().unwrap()
.get(&hint.to_string())
.cloned()
.ok_or(coapum::dtls::Error::ErrIdentityNoPsk)
})),
psk_identity_hint: Some("coapum-server".as_bytes().to_vec()),
cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256],
extended_master_secret: ExtendedMasterSecretType::Require,
..Default::default()
};
let server_config = config::Config {
dtls_cfg: dtls_config,
..Default::default()
};
serve::serve("127.0.0.1:5684".to_string(), server_config, router).await?;
Ok(())
}
use coapum::{
dtls::{cipher_suite::CipherSuiteId, config::Config, conn::DTLSConn},
util::Conn,
CoapRequest, RequestType, Packet,
};
use tokio::net::UdpSocket;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup UDP connection
let socket = Arc::new(UdpSocket::bind("127.0.0.1:0").await?);
socket.connect("127.0.0.1:5684").await?;
// Configure DTLS client
let config = Config {
psk: Some(Arc::new(|_hint: &[u8]| Ok("secret_key".as_bytes().to_vec()))),
psk_identity_hint: Some("device123".as_bytes().to_vec()),
cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256],
..Default::default()
};
let dtls_conn: Arc<dyn Conn + Send + Sync> =
Arc::new(DTLSConn::new(socket, config, true, None).await?);
// Send CoAP request
let mut request = CoapRequest::new();
request.set_method(RequestType::Get);
request.set_path("status");
dtls_conn.send(&request.message.to_bytes()?).await?;
// Receive response
let mut buffer = vec![0u8; 1024];
let n = dtls_conn.recv(&mut buffer).await?;
let response = Packet::from_bytes(&buffer[0..n])?;
println!("Response: {}", String::from_utf8_lossy(&response.payload));
Ok(())
}
Coapum provides an ergonomic routing system inspired by web frameworks:
let router = RouterBuilder::new(state, observer)
.get("/users/:id", get_user) // GET with path parameter
.post("/users", create_user) // POST with JSON body
.put("/users/:id", update_user) // PUT with path + body
.delete("/users/:id", delete_user) // DELETE
.observe("/sensors/:id", get_sensor, notify_sensor) // Observer pattern
.build();
Coapum automatically extracts data from requests using type-safe extractors:
Path<T> - Extract path parametersJson<T> - Parse JSON payloadCbor<T> - Parse CBOR payloadSenML - Parse SenML (Sensor Measurement Lists) payloadBytes - Raw byte payloadRaw - Raw payload dataState<T> - Access shared application stateIdentity - Client identity from DTLSObserveFlag - CoAP observe optionSource - Request source informationasync fn handler(
Path(user_id): Path<u32>, // Extract :id as u32
Json(user_data): Json<UserData>, // Parse JSON body
State(db): State<Database>, // Access shared state
) -> Result<Json<User>, StatusCode> {
// Handler logic here
}
// SenML handler example
async fn sensor_handler(
Path(device_id): Path<String>, // Extract device ID
SenML(measurements): SenML, // Parse SenML payload
) -> Result<StatusCode, StatusCode> {
println!("Device {}: {} measurements", device_id, measurements.len());
Ok(StatusCode::Changed)
}
CoAP's observe mechanism is fully supported with persistent storage:
// Register observer endpoint
.observe("/temperature", get_temp, notify_temp)
// Get handler - returns current value
async fn get_temp() -> Json<Temperature> {
Json(Temperature { value: 23.5 })
}
// Notify handler - called when sending updates to observers
async fn notify_temp() -> Json<Temperature> {
Json(read_current_temperature())
}
Coapum includes built-in support for Sensor Measurement Lists (SenML) RFC 8428:
use coapum::extract::SenML;
use coapum_senml::SenMLBuilder;
// Handler accepting SenML sensor data
async fn sensor_data(SenML(measurements): SenML) -> SenML {
println!("Received {} measurements", measurements.len());
// Create response using SenML builder
let response = SenMLBuilder::new()
.base_name("urn:controller/")
.add_string_value("status", "received")
.add_value("count", measurements.len() as f64)
.build();
SenML(response)
}
SenML supports multiple formats:
xml feature)Choose from multiple observer storage backends:
// In-memory (for testing/development)
let observer = MemObserver::new();
// Persistent storage with Sled
let observer = SledObserver::new("observers.db").unwrap();
use coapum::config::Config;
let config = Config {
dtls_cfg: dtls_config,
max_message_size: 1024,
ack_timeout: Duration::from_secs(2),
max_retransmit: 4,
..Default::default()
};
use coapum::dtls::config::{Config, ExtendedMasterSecretType};
let dtls_config = Config {
psk: Some(Arc::new(psk_callback)),
psk_identity_hint: Some("server".as_bytes().to_vec()),
cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256],
extended_master_secret: ExtendedMasterSecretType::Require,
..Default::default()
};
[dependencies]
coapum = { version = "0.2.0", features = ["sled-observer"] }
coapum-senml = { version = "0.1.0", features = ["json", "cbor", "xml"] }
sled-observer - Enable Sled database backend for observers (default)json - JSON serialization support (default)cbor - CBOR serialization support (default)xml - XML serialization supportvalidation - Input validation supportThe examples/ directory contains complete examples:
cbor_server.rs - CBOR payload handling with device state managementcbor_client.rs - CBOR client implementationraw_server.rs - Raw payload handlingraw_client.rs - Raw client implementationsenml_example.rs - Advanced SenML payload handling with time-series datasenml_simple.rs - Simple SenML payload handling demonstrationconcurrency.rs - Concurrent request handlingdynamic_client_management.rs - Dynamic client management exampleexternal_state_updates.rs - External state update handlingRun an example:
# Start CBOR server
cargo run --example cbor_server
# In another terminal, run client
cargo run --example cbor_client
Run the test suite:
# Run all tests
cargo test
# Run with logging
RUST_LOG=debug cargo test
# Run specific test module
cargo test router
# Run router benchmarks
cargo bench
Install grcov and generate coverage reports:
cargo install grcov
# Generate coverage data
CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' \
LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test
# Generate HTML report
grcov . --binary-path ./target/debug/ -s . -t html \
--branch --ignore-not-existing --ignore "target/*" \
-o target/coverage/
# Generate LCOV report
grcov . --binary-path ./target/debug/ -s . -t lcov \
--branch --ignore-not-existing --ignore "target/*" \
-o target/coverage/tests.lcov
Coapum is built with the following principles:
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
# Clone the repository
git clone https://github.com/username/coapum.git
cd coapum
# Run tests
cargo test
# Run clippy for linting
cargo clippy
# Format code
cargo fmt
This project is licensed under either of
at your option.
For more information, see the API documentation.