use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use coap_lite::{CoapOption, MessageClass, MessageType, Packet, RequestType};
use pamoja_core::{Error, Result, Transport};
use tokio::net::UdpSocket;
use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinHandle;
type PendingAcks = Arc<Mutex<HashMap<u16, oneshot::Sender<()>>>>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Reliability {
NonConfirmable,
Confirmable,
}
#[derive(Clone, Debug)]
pub struct CoapConfig {
host: String,
port: u16,
bind: String,
reliability: Reliability,
ack_timeout: Duration,
max_retransmits: u32,
}
impl CoapConfig {
pub fn new(host: impl Into<String>, port: u16) -> Self {
Self {
host: host.into(),
port,
bind: "0.0.0.0:0".to_owned(),
reliability: Reliability::Confirmable,
ack_timeout: Duration::from_secs(2),
max_retransmits: 4,
}
}
pub fn bind(mut self, addr: impl Into<String>) -> Self {
self.bind = addr.into();
self
}
pub fn reliability(mut self, reliability: Reliability) -> Self {
self.reliability = reliability;
self
}
pub fn ack_timeout(mut self, timeout: Duration) -> Self {
self.ack_timeout = timeout;
self
}
pub fn max_retransmits(mut self, count: u32) -> Self {
self.max_retransmits = count;
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Message {
pub topic: String,
pub payload: Vec<u8>,
}
pub struct CoapTransport {
config: CoapConfig,
socket: Option<Arc<UdpSocket>>,
incoming: Option<mpsc::UnboundedReceiver<Message>>,
pending: PendingAcks,
pump: Option<JoinHandle<()>>,
next_id: u16,
next_token: u16,
}
impl CoapTransport {
pub fn new(config: CoapConfig) -> Self {
Self {
config,
socket: None,
incoming: None,
pending: Arc::new(Mutex::new(HashMap::new())),
pump: None,
next_id: 0,
next_token: 0,
}
}
pub fn is_connected(&self) -> bool {
self.socket.is_some()
}
pub async fn recv(&mut self) -> Result<Option<Message>> {
let incoming = self.incoming.as_mut().ok_or(Error::Closed)?;
Ok(incoming.recv().await)
}
pub async fn disconnect(&mut self) -> Result<()> {
if let Some(pump) = self.pump.take() {
pump.abort();
}
self.socket = None;
self.incoming = None;
Ok(())
}
fn next_message_id(&mut self) -> u16 {
let id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
id
}
fn next_request_token(&mut self) -> Vec<u8> {
let token = self.next_token;
self.next_token = self.next_token.wrapping_add(1);
token.to_be_bytes().to_vec()
}
async fn send_confirmable(&mut self, id: u16, bytes: &[u8], socket: &UdpSocket) -> Result<()> {
let mut timeout = self.config.ack_timeout;
for _ in 0..=self.config.max_retransmits {
let (tx, rx) = oneshot::channel();
self.pending.lock().expect("pending lock").insert(id, tx);
socket
.send(bytes)
.await
.map_err(|err| Error::Transport(err.to_string()))?;
match tokio::time::timeout(timeout, rx).await {
Ok(Ok(())) => return Ok(()),
Ok(Err(_)) => return Err(Error::Closed),
Err(_) => {
self.pending.lock().expect("pending lock").remove(&id);
timeout = timeout.saturating_mul(2);
}
}
}
Err(Error::Transport(format!(
"no acknowledgement for message {id}"
)))
}
}
impl Transport for CoapTransport {
async fn connect(&mut self) -> Result<()> {
let server = tokio::net::lookup_host((self.config.host.as_str(), self.config.port))
.await
.map_err(|err| Error::Transport(err.to_string()))?
.next()
.ok_or_else(|| Error::Transport(format!("could not resolve {}", self.config.host)))?;
let socket = UdpSocket::bind(&self.config.bind)
.await
.map_err(|err| Error::Transport(err.to_string()))?;
socket
.connect(server)
.await
.map_err(|err| Error::Transport(err.to_string()))?;
let socket = Arc::new(socket);
let (tx, rx) = mpsc::unbounded_channel();
let pending = Arc::clone(&self.pending);
let pump_socket = Arc::clone(&socket);
let pump = tokio::spawn(async move {
let mut buf = vec![0u8; 1500];
while let Ok(len) = pump_socket.recv(&mut buf).await {
let Ok(packet) = Packet::from_bytes(&buf[..len]) else {
continue;
};
if !dispatch(packet, &pending, &tx, &pump_socket).await {
break;
}
}
});
self.socket = Some(socket);
self.incoming = Some(rx);
self.pump = Some(pump);
Ok(())
}
async fn send(&mut self, topic: &str, payload: &[u8]) -> Result<()> {
let socket = self.socket.clone().ok_or(Error::Closed)?;
let id = self.next_message_id();
let token = self.next_request_token();
let mut packet = Packet::new();
packet.header.set_version(1);
packet
.header
.set_type(message_type(self.config.reliability));
packet.header.code = MessageClass::Request(RequestType::Put);
packet.header.message_id = id;
packet.set_token(token);
add_path(&mut packet, topic);
packet.payload = payload.to_vec();
let bytes = packet
.to_bytes()
.map_err(|err| Error::Codec(err.to_string()))?;
match self.config.reliability {
Reliability::NonConfirmable => socket
.send(&bytes)
.await
.map(|_| ())
.map_err(|err| Error::Transport(err.to_string())),
Reliability::Confirmable => self.send_confirmable(id, &bytes, &socket).await,
}
}
async fn subscribe(&mut self, topic: &str) -> Result<()> {
let socket = self.socket.clone().ok_or(Error::Closed)?;
let id = self.next_message_id();
let token = self.next_request_token();
let mut packet = Packet::new();
packet.header.set_version(1);
packet.header.set_type(MessageType::Confirmable);
packet.header.code = MessageClass::Request(RequestType::Get);
packet.header.message_id = id;
packet.set_token(token);
packet.add_option(CoapOption::Observe, Vec::new());
add_path(&mut packet, topic);
let bytes = packet
.to_bytes()
.map_err(|err| Error::Codec(err.to_string()))?;
self.send_confirmable(id, &bytes, &socket).await
}
}
fn message_type(reliability: Reliability) -> MessageType {
match reliability {
Reliability::NonConfirmable => MessageType::NonConfirmable,
Reliability::Confirmable => MessageType::Confirmable,
}
}
fn add_path(packet: &mut Packet, topic: &str) {
for segment in topic.split('/').filter(|segment| !segment.is_empty()) {
packet.add_option(CoapOption::UriPath, segment.as_bytes().to_vec());
}
}
fn path_from_packet(packet: &Packet) -> String {
match packet.get_option(CoapOption::UriPath) {
Some(segments) => segments
.iter()
.map(|segment| String::from_utf8_lossy(segment).into_owned())
.collect::<Vec<_>>()
.join("/"),
None => String::new(),
}
}
async fn dispatch(
packet: Packet,
pending: &PendingAcks,
tx: &mpsc::UnboundedSender<Message>,
socket: &UdpSocket,
) -> bool {
match packet.header.get_type() {
MessageType::Acknowledgement => {
if let Some(waiter) = pending
.lock()
.expect("pending lock")
.remove(&packet.header.message_id)
{
let _ = waiter.send(());
}
if packet.get_option(CoapOption::Observe).is_some() {
return enqueue(packet, tx);
}
true
}
MessageType::Confirmable => {
acknowledge(&packet, socket).await;
enqueue(packet, tx)
}
MessageType::NonConfirmable => enqueue(packet, tx),
MessageType::Reset => {
if let Some(waiter) = pending
.lock()
.expect("pending lock")
.remove(&packet.header.message_id)
{
let _ = waiter.send(());
}
true
}
}
}
async fn acknowledge(packet: &Packet, socket: &UdpSocket) {
let mut ack = Packet::new();
ack.header.set_version(1);
ack.header.set_type(MessageType::Acknowledgement);
ack.header.code = MessageClass::Empty;
ack.header.message_id = packet.header.message_id;
if let Ok(bytes) = ack.to_bytes() {
let _ = socket.send(&bytes).await;
}
}
fn enqueue(packet: Packet, tx: &mpsc::UnboundedSender<Message>) -> bool {
let message = Message {
topic: path_from_packet(&packet),
payload: packet.payload,
};
tx.send(message).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reliability_defaults_to_confirmable() {
let config = CoapConfig::new("localhost", 5683);
assert_eq!(config.reliability, Reliability::Confirmable);
}
#[test]
fn setters_update_the_configuration() {
let config = CoapConfig::new("localhost", 5683)
.reliability(Reliability::NonConfirmable)
.ack_timeout(Duration::from_millis(250))
.max_retransmits(1)
.bind("127.0.0.1:0");
assert_eq!(config.reliability, Reliability::NonConfirmable);
assert_eq!(config.ack_timeout, Duration::from_millis(250));
assert_eq!(config.max_retransmits, 1);
assert_eq!(config.bind, "127.0.0.1:0");
}
#[test]
fn path_round_trips_through_uri_path_options() {
let mut packet = Packet::new();
add_path(&mut packet, "sensors/1/temperature");
assert_eq!(path_from_packet(&packet), "sensors/1/temperature");
}
#[test]
fn leading_and_repeated_slashes_are_ignored() {
let mut packet = Packet::new();
add_path(&mut packet, "/sensors//1/");
assert_eq!(path_from_packet(&packet), "sensors/1");
}
#[tokio::test]
async fn send_before_connect_reports_closed() {
let mut transport = CoapTransport::new(CoapConfig::new("localhost", 5683));
assert!(matches!(
transport.send("t", b"x").await,
Err(Error::Closed)
));
}
#[tokio::test]
async fn subscribe_before_connect_reports_closed() {
let mut transport = CoapTransport::new(CoapConfig::new("localhost", 5683));
assert!(matches!(transport.subscribe("t").await, Err(Error::Closed)));
}
#[tokio::test]
async fn recv_before_connect_reports_closed() {
let mut transport = CoapTransport::new(CoapConfig::new("localhost", 5683));
assert!(matches!(transport.recv().await, Err(Error::Closed)));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn confirmable_send_without_a_server_times_out() {
let config = CoapConfig::new("127.0.0.1", 1)
.ack_timeout(Duration::from_millis(20))
.max_retransmits(1);
let mut transport = CoapTransport::new(config);
transport.connect().await.expect("bind socket");
assert!(transport.send("sensors/1", b"x").await.is_err());
}
}