use std::fmt;
use std::time::Duration;
use ebus_homie_tokio::device::{
AttachError, BuildError, DeviceDriver, DeviceHandle, DeviceOptions, PairDriver, TreeBuilder,
attach_device, attach_pair,
};
use ebus_homie_tokio::ebus_homie::controller::Config;
use ebus_homie_tokio::ebus_mqtt::LastWill;
use ebus_homie_tokio::{ControllerHandle, EventReceiver};
use rumqttc::{AsyncClient, MqttOptions};
use crate::Error;
use crate::conn::{self, Conn, DeviceConf, REQUEST_CAPACITY, Redirect, Reresolve};
const MAX_INFLIGHT: u16 = 20;
const PACKET_OVERHEAD: usize = 1024;
#[derive(Debug)]
#[non_exhaustive]
pub struct RunningDevice {
pub handle: DeviceHandle,
pub task: DeviceTask,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct RunningPair {
pub device: DeviceHandle,
pub controller: ControllerHandle,
pub events: EventReceiver,
pub task: PairTask,
}
pub struct DeviceTask {
driver: DeviceDriver,
conn: Conn,
device: DeviceConf,
}
impl fmt::Debug for DeviceTask {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DeviceTask")
.field("driver", &self.driver)
.field("broker", &self.conn.eventloop.mqtt_options.broker_address())
.finish_non_exhaustive()
}
}
pub struct PairTask {
driver: PairDriver,
conn: Conn,
device: DeviceConf,
}
impl fmt::Debug for PairTask {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PairTask")
.field("driver", &self.driver)
.field("broker", &self.conn.eventloop.mqtt_options.broker_address())
.finish_non_exhaustive()
}
}
impl DeviceTask {
pub fn reresolve(mut self, f: Reresolve) -> Self {
self.conn.redirect = Some(Redirect::new(f, Some(self.device.clone())));
self
}
pub async fn run(self) -> Result<(), Error> {
conn::run(self.driver.run(), self.conn)
.await
.map_err(Error::Driver)
}
}
impl PairTask {
pub fn reresolve(mut self, f: Reresolve) -> Self {
self.conn.redirect = Some(Redirect::new(f, Some(self.device.clone())));
self
}
pub async fn run(self) -> Result<(), Error> {
conn::run(self.driver.run(), self.conn)
.await
.map_err(Error::Driver)
}
}
pub(crate) fn configure(mqtt: &mut MqttOptions, will: &LastWill, max_description: usize) {
mqtt.set_last_will(rumqttc::LastWill::new(
will.topic.clone(),
will.payload.clone(),
conn::qos(will.qos),
will.retain,
));
mqtt.set_clean_session(true);
let inflight = mqtt.inflight().min(MAX_INFLIGHT);
mqtt.set_inflight(inflight);
let incoming = mqtt.max_packet_size();
mqtt.set_max_packet_size(incoming, max_description.saturating_add(PACKET_OVERHEAD));
}
fn options(tree: &mut TreeBuilder, mut opts: DeviceOptions) -> DeviceOptions {
tree.set_receipts(true);
opts.link_up_discards_queued = true;
opts
}
pub fn device(
mut mqtt: MqttOptions,
mut tree: TreeBuilder,
opts: DeviceOptions,
) -> Result<RunningDevice, BuildError> {
let opts = options(&mut tree, opts);
let max = tree.config().max_description_bytes;
let a = attach_device(tree, opts)?;
configure(&mut mqtt, &a.last_will, max);
let (client, eventloop) = AsyncClient::new(mqtt, REQUEST_CAPACITY);
Ok(RunningDevice {
handle: a.handle,
task: DeviceTask {
driver: a.driver,
conn: Conn {
inbox: a.inbox,
commands: a.commands,
client,
eventloop,
receipts: true,
flush_timeout: Duration::from_millis(opts.flush_timeout_ms),
redirect: None,
},
device: DeviceConf {
will: a.last_will,
max_description: max,
},
},
})
}
pub fn pair(
mut mqtt: MqttOptions,
mut tree: TreeBuilder,
config: Config,
opts: DeviceOptions,
) -> Result<RunningPair, AttachError> {
let opts = options(&mut tree, opts);
let max = tree.config().max_description_bytes;
let a = attach_pair(tree, config, opts)?;
configure(&mut mqtt, &a.last_will, max);
let (client, eventloop) = AsyncClient::new(mqtt, REQUEST_CAPACITY);
Ok(RunningPair {
device: a.device,
controller: a.controller,
events: a.events,
task: PairTask {
driver: a.driver,
conn: Conn {
inbox: a.inbox,
commands: a.commands,
client,
eventloop,
receipts: true,
flush_timeout: Duration::from_millis(opts.flush_timeout_ms),
redirect: None,
},
device: DeviceConf {
will: a.last_will,
max_description: max,
},
},
})
}
#[cfg(test)]
mod tests {
use super::*;
use ebus_homie_tokio::device::{DeviceSpec, TreeConfig};
fn send_check<T: Send>(_: &T) {}
fn tree() -> TreeBuilder {
TreeBuilder::new(TreeConfig::default(), "r", DeviceSpec::default()).expect("tree")
}
#[test]
fn device_connections_carry_the_will_and_the_limits() {
let mut mqtt = MqttOptions::new("r", "localhost", 1883);
mqtt.set_clean_session(false);
let will = LastWill::new(
"ebus/5/r/$state",
b"lost".to_vec(),
ebus_homie_tokio::ebus_mqtt::QoS::ExactlyOnce,
true,
);
configure(&mut mqtt, &will, 10_000);
let w = mqtt.last_will().expect("will");
assert_eq!(w.topic, "ebus/5/r/$state");
assert_eq!(&w.message[..], b"lost");
assert_eq!(w.qos, rumqttc::QoS::ExactlyOnce);
assert!(w.retain);
assert!(mqtt.clean_session());
assert_eq!(mqtt.inflight(), 20);
mqtt.set_inflight(5);
configure(&mut mqtt, &will, 10_000);
assert_eq!(mqtt.inflight(), 5, "a lower cap is kept");
}
async fn publish_reaches_broker(device: bool, payload: usize) -> bool {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let port = listener.local_addr().expect("address").port();
let broker = tokio::spawn(async move {
let (mut s, _) = listener.accept().await.expect("accept");
let mut buf = vec![0u8; 4096];
let _ = s.read(&mut buf).await;
s.write_all(&[0x20, 2, 0, 0]).await.expect("CONNACK");
let mut total = 0;
while total < payload {
match s.read(&mut buf).await {
Ok(0) | Err(_) => return false,
Ok(n) => total += n,
}
}
true
});
let mut mqtt = MqttOptions::new("r", "127.0.0.1", port);
if device {
let will = LastWill::new(
"ebus/5/r/$state",
b"lost".to_vec(),
ebus_homie_tokio::ebus_mqtt::QoS::AtLeastOnce,
true,
);
configure(&mut mqtt, &will, payload);
}
let (client, mut eventloop) = AsyncClient::new(mqtt, 4);
client
.publish("t", rumqttc::QoS::AtMostOnce, false, vec![b'x'; payload])
.await
.expect("queued");
let poller = tokio::spawn(async move { while eventloop.poll().await.is_ok() {} });
let reached = tokio::time::timeout(Duration::from_secs(5), broker)
.await
.is_ok_and(|r| r.expect("broker task"));
poller.abort();
reached
}
#[tokio::test]
async fn device_connections_send_a_description_of_the_configured_size() {
assert!(!publish_reaches_broker(false, 12_000).await);
assert!(publish_reaches_broker(true, 12_000).await);
}
#[test]
fn tasks_are_send() {
let mqtt = MqttOptions::new("r", "localhost", 1883);
let run = device(mqtt.clone(), tree(), DeviceOptions::default()).expect("device");
send_check(&run.task.run());
let run = device(mqtt.clone(), tree(), DeviceOptions::default()).expect("device");
send_check(
&run.task
.reresolve(Box::new(|| Box::pin(async { None })))
.run(),
);
let run = pair(mqtt, tree(), Config::wildcard(), DeviceOptions::default()).expect("pair");
send_check(
&run.task
.reresolve(Box::new(|| Box::pin(async { None })))
.run(),
);
}
#[tokio::test]
async fn abort_while_the_broker_is_down_ends_the_task() {
let port = std::net::TcpListener::bind("127.0.0.1:0")
.and_then(|l| l.local_addr())
.expect("free port")
.port();
let mqtt = MqttOptions::new("r", "127.0.0.1", port);
let run = device(mqtt, tree(), DeviceOptions::default()).expect("device");
let task = tokio::spawn(run.task.run());
tokio::time::sleep(Duration::from_millis(100)).await;
run.handle.abort();
let ended = tokio::time::timeout(Duration::from_secs(10), task).await;
assert!(matches!(ended, Ok(Ok(Ok(())))), "{ended:?}");
}
#[tokio::test]
async fn shutdown_while_the_broker_is_down_ends_the_task() {
let port = std::net::TcpListener::bind("127.0.0.1:0")
.and_then(|l| l.local_addr())
.expect("free port")
.port();
let mqtt = MqttOptions::new("r", "127.0.0.1", port);
let run = device(mqtt, tree(), DeviceOptions::default()).expect("device");
let task = tokio::spawn(run.task.run());
tokio::time::sleep(Duration::from_millis(100)).await;
run.handle
.shutdown(ebus_homie_tokio::device::Shutdown::Announce)
.await;
let ended = tokio::time::timeout(Duration::from_secs(10), task).await;
assert!(matches!(ended, Ok(Ok(Ok(())))), "{ended:?}");
}
}