use std::{net::SocketAddr, sync::Arc};
use tokio::net::UdpSocket;
use coapum::{
dtls::{Error, cipher_suite::CipherSuiteId, config::Config, conn::DTLSConn},
util::Conn,
{CoapRequest, ContentFormat, Packet, RequestType},
};
use serde::{Deserialize, Serialize};
const IDENTITY: &[u8] = "goobie!".as_bytes();
const PSK: &[u8] = "63ef2024b1de6417f856fab7005d38f6".as_bytes();
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DeviceState {
temperature: f32,
humidity: f32,
battery_level: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ApiResponse {
status: String,
message: String,
}
async fn send_request(
dtls_conn: &DTLSConn,
method: RequestType,
path: &str,
payload: Option<Vec<u8>>,
content_format: Option<ContentFormat>,
) -> Result<Packet, Box<dyn std::error::Error>> {
let mut request: CoapRequest<SocketAddr> = CoapRequest::new();
request.set_method(method);
request.set_path(path);
if let Some(payload) = payload {
request.message.payload = payload;
}
if let Some(format) = content_format {
request.message.set_content_format(format);
}
log::info!("Sending {:?} request to {}", method, path);
match dtls_conn.send(&request.message.to_bytes().unwrap()).await {
Ok(n) => {
log::info!("Wrote {} bytes", n);
}
Err(e) => {
log::error!("Error writing: {}", e);
return Err(e.into());
}
};
let mut buffer = vec![0u8; 1024];
if let Ok(n) = dtls_conn.recv(&mut buffer).await {
log::debug!("Read {} bytes", n);
let packet = Packet::from_bytes(&buffer[0..n]).unwrap();
log::info!("Response status: {:?}", packet.header.code);
if !packet.payload.is_empty() {
log::debug!("Response payload: {} bytes", packet.payload.len());
}
Ok(packet)
} else {
Err("Failed to receive response".into())
}
}
async fn test_device_state_endpoints(
dtls_conn: &DTLSConn,
) -> Result<(), Box<dyn std::error::Error>> {
println!("\n=== Testing Device State Endpoints ===");
let device_id = "device123";
let path = format!(".d/{}", device_id);
println!("\n1. Testing POST (update device state)");
let device_state = DeviceState {
temperature: 25.5,
humidity: 60.0,
battery_level: 85,
};
let cbor_payload = {
let mut buffer = Vec::new();
ciborium::ser::into_writer(&device_state, &mut buffer)?;
buffer
};
let response = send_request(
dtls_conn,
RequestType::Post,
&path,
Some(cbor_payload),
Some(ContentFormat::ApplicationCBOR),
)
.await?;
if !response.payload.is_empty() {
let api_response: ApiResponse = ciborium::de::from_reader(&response.payload[..])?;
println!("API Response: {:?}", api_response);
}
println!("\n2. Testing GET (retrieve device state)");
let response = send_request(dtls_conn, RequestType::Get, &path, None, None).await?;
if !response.payload.is_empty() {
let retrieved_state: DeviceState = ciborium::de::from_reader(&response.payload[..])?;
println!("Retrieved device state: {:?}", retrieved_state);
}
println!("\n3. Testing DELETE (remove device state)");
let _response = send_request(dtls_conn, RequestType::Delete, &path, None, None).await?;
Ok(())
}
async fn test_stream_endpoints(dtls_conn: &DTLSConn) -> Result<(), Box<dyn std::error::Error>> {
println!("\n=== Testing Stream Endpoints ===");
let stream_id = "stream456";
let path = format!(".s/{}", stream_id);
println!("\n1. Testing POST (stream data)");
let stream_data = b"Sample stream data payload";
let _response = send_request(
dtls_conn,
RequestType::Post,
&path,
Some(stream_data.to_vec()),
None,
)
.await?;
Ok(())
}
async fn test_utility_endpoints(dtls_conn: &DTLSConn) -> Result<(), Box<dyn std::error::Error>> {
println!("\n=== Testing Utility Endpoints ===");
println!("\n1. Testing echo endpoint");
let echo_data = b"Hello, CoAP world!";
let response = send_request(
dtls_conn,
RequestType::Put,
"echo",
Some(echo_data.to_vec()),
None,
)
.await?;
if !response.payload.is_empty() {
let echoed = String::from_utf8_lossy(&response.payload);
println!("Echoed back: {}", echoed);
}
println!("\n2. Testing hello endpoint");
let hello_data = b"Hello from GET!";
let response = send_request(
dtls_conn,
RequestType::Get,
"hello",
Some(hello_data.to_vec()),
None,
)
.await?;
if !response.payload.is_empty() {
let echoed = String::from_utf8_lossy(&response.payload);
println!("Hello response: {}", echoed);
}
println!("\n3. Testing ping endpoint");
let _response = send_request(dtls_conn, RequestType::Get, "", None, None).await?;
Ok(())
}
async fn test_error_conditions(dtls_conn: &DTLSConn) -> Result<(), Box<dyn std::error::Error>> {
println!("\n=== Testing Error Conditions ===");
println!("\n1. Testing invalid path");
let _response =
send_request(dtls_conn, RequestType::Get, "nonexistent/path", None, None).await?;
println!("\n2. Testing invalid CBOR data");
let invalid_cbor = vec![0xFF, 0xFF, 0xFF, 0xFF];
let _response = send_request(
dtls_conn,
RequestType::Post,
".d/test",
Some(invalid_cbor),
Some(ContentFormat::ApplicationCBOR),
)
.await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
println!("🚀 Ergonomic CoAP Client Starting!");
println!("Testing the new ergonomic server API...");
let local_addr = "127.0.0.1:0";
let server_addr = "127.0.0.1:5684";
let conn = Arc::new(UdpSocket::bind(local_addr).await?);
conn.connect(server_addr).await?;
println!("📡 Connected to server at {}", server_addr);
let config = Config {
psk: Some(Arc::new(|hint: &[u8]| -> Result<Vec<u8>, Error> {
log::info!(
"Server's hint: {}",
String::from_utf8(hint.to_vec()).unwrap()
);
Ok(PSK.to_vec())
})),
psk_identity_hint: Some(IDENTITY.to_vec()),
cipher_suites: vec![CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256],
..Default::default()
};
let dtls_conn = Arc::new(DTLSConn::new(conn, config, true, None).await?);
println!("🔒 DTLS connection established");
match test_device_state_endpoints(&dtls_conn).await {
Ok(_) => println!("✅ Device state endpoints test passed"),
Err(e) => println!("❌ Device state endpoints test failed: {}", e),
}
match test_stream_endpoints(&dtls_conn).await {
Ok(_) => println!("✅ Stream endpoints test passed"),
Err(e) => println!("❌ Stream endpoints test failed: {}", e),
}
match test_utility_endpoints(&dtls_conn).await {
Ok(_) => println!("✅ Utility endpoints test passed"),
Err(e) => println!("❌ Utility endpoints test failed: {}", e),
}
match test_error_conditions(&dtls_conn).await {
Ok(_) => println!("✅ Error conditions test passed"),
Err(e) => println!("❌ Error conditions test failed: {}", e),
}
println!("\n🎉 All tests completed!");
println!("The ergonomic API is working correctly!");
Ok(())
}