coapum

Crates.iocoapum
lib.rscoapum
version0.2.0
created_at2025-08-19 19:20:51.191371+00
updated_at2025-08-19 19:20:51.191371+00
descriptionA modern, ergonomic CoAP (Constrained Application Protocol) library for Rust with support for DTLS, observers, and asynchronous handlers
homepagehttps://github.com/jaredwolff/coapum
repositoryhttps://github.com/jaredwolff/coapum
max_upload_size
id1802327
size583,212
Jared Wolff (jaredwolff)

documentation

https://docs.rs/coapum

README

Coapum

A modern, ergonomic CoAP (Constrained Application Protocol) library for Rust with support for DTLS, observers, and asynchronous handlers.

Crates.io Documentation License

Features

  • ๐Ÿš€ Async/await support - Built on Tokio for high-performance async networking
  • ๐Ÿ›ก๏ธ DTLS security - Full DTLS 1.2 support with PSK authentication
  • ๐ŸŽฏ Ergonomic routing - Express-like routing with automatic parameter extraction
  • ๐Ÿ‘๏ธ Observer pattern - CoAP observe support with persistent storage backends
  • ๐Ÿ“ฆ Multiple payload formats - JSON, CBOR, and raw byte support
  • ๐Ÿ”ง Type-safe extractors - Automatic request parsing with compile-time guarantees
  • ๐Ÿ—„๏ธ Pluggable storage - Memory and Sled database backends for observers
  • ๐Ÿงช Comprehensive testing - High test coverage with benchmarks

Quick Start

Add Coapum to your Cargo.toml:

[dependencies]
coapum = "0.2.0"

# For standalone SenML usage
coapum-senml = "0.1.0"

Basic Server

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(())
}

Secure DTLS Server

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(())
}

Client Example

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(())
}

Core Concepts

Routing

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();

Extractors

Coapum automatically extracts data from requests using type-safe extractors:

  • Path<T> - Extract path parameters
  • Json<T> - Parse JSON payload
  • Cbor<T> - Parse CBOR payload
  • SenML - Parse SenML (Sensor Measurement Lists) payload
  • Bytes - Raw byte payload
  • Raw - Raw payload data
  • State<T> - Access shared application state
  • Identity - Client identity from DTLS
  • ObserveFlag - CoAP observe option
  • Source - Request source information
async 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)
}

Observer Pattern

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())
}

SenML Support

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:

  • JSON - Standard SenML JSON format
  • CBOR - Compact binary format for IoT devices
  • XML - Legacy XML format (with xml feature)

Storage Backends

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();

Configuration

Server Configuration

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()
};

DTLS Configuration

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()
};

Feature Flags

[dependencies]
coapum = { version = "0.2.0", features = ["sled-observer"] }
coapum-senml = { version = "0.1.0", features = ["json", "cbor", "xml"] }

Coapum Features

  • sled-observer - Enable Sled database backend for observers (default)

SenML Features

  • json - JSON serialization support (default)
  • cbor - CBOR serialization support (default)
  • xml - XML serialization support
  • validation - Input validation support

Examples

The examples/ directory contains complete examples:

  • cbor_server.rs - CBOR payload handling with device state management
  • cbor_client.rs - CBOR client implementation
  • raw_server.rs - Raw payload handling
  • raw_client.rs - Raw client implementation
  • senml_example.rs - Advanced SenML payload handling with time-series data
  • senml_simple.rs - Simple SenML payload handling demonstration
  • concurrency.rs - Concurrent request handling
  • dynamic_client_management.rs - Dynamic client management example
  • external_state_updates.rs - External state update handling

Run an example:

# Start CBOR server
cargo run --example cbor_server

# In another terminal, run client
cargo run --example cbor_client

Testing

Run the test suite:

# Run all tests
cargo test

# Run with logging
RUST_LOG=debug cargo test

# Run specific test module
cargo test router

Benchmarks

# Run router benchmarks
cargo bench

Code Coverage

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

Architecture

Coapum is built with the following principles:

  • Async-first: Built on Tokio for high-performance async I/O
  • Type safety: Extensive use of Rust's type system to prevent runtime errors
  • Ergonomics: API design inspired by modern web frameworks
  • Modularity: Pluggable components for storage, security, and serialization
  • Performance: Zero-copy parsing and efficient routing algorithms

Key Components

  • Router: Route matching and handler dispatch
  • Extractors: Type-safe request data extraction
  • Handlers: Function-based request handling
  • Observers: CoAP observe pattern implementation
  • DTLS: Secure transport layer
  • Config: Server and security configuration

Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

Development Setup

# 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

License

This project is licensed under either of

at your option.

Acknowledgments


For more information, see the API documentation.

Commit count: 89

cargo fmt