1use std::fmt;
4use std::time::Duration;
5
6use ebus_homie_tokio::device::{
7 AttachError, BuildError, DeviceDriver, DeviceHandle, DeviceOptions, PairDriver, TreeBuilder,
8 attach_device, attach_pair,
9};
10use ebus_homie_tokio::ebus_homie::controller::Config;
11use ebus_homie_tokio::ebus_mqtt::LastWill;
12use ebus_homie_tokio::{ControllerHandle, EventReceiver};
13use rumqttc::{AsyncClient, MqttOptions};
14
15use crate::Error;
16use crate::conn::{self, Conn, DeviceConf, REQUEST_CAPACITY, Redirect, Reresolve};
17
18const MAX_INFLIGHT: u16 = 20;
21
22const PACKET_OVERHEAD: usize = 1024;
24
25#[derive(Debug)]
27#[non_exhaustive]
28pub struct RunningDevice {
29 pub handle: DeviceHandle,
31 pub task: DeviceTask,
33}
34
35#[derive(Debug)]
37#[non_exhaustive]
38pub struct RunningPair {
39 pub device: DeviceHandle,
41 pub controller: ControllerHandle,
43 pub events: EventReceiver,
45 pub task: PairTask,
47}
48
49pub struct DeviceTask {
52 driver: DeviceDriver,
53 conn: Conn,
54 device: DeviceConf,
56}
57
58impl fmt::Debug for DeviceTask {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.debug_struct("DeviceTask")
61 .field("driver", &self.driver)
62 .field("broker", &self.conn.eventloop.mqtt_options.broker_address())
63 .finish_non_exhaustive()
64 }
65}
66
67pub struct PairTask {
70 driver: PairDriver,
71 conn: Conn,
72 device: DeviceConf,
74}
75
76impl fmt::Debug for PairTask {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 f.debug_struct("PairTask")
79 .field("driver", &self.driver)
80 .field("broker", &self.conn.eventloop.mqtt_options.broker_address())
81 .finish_non_exhaustive()
82 }
83}
84
85impl DeviceTask {
86 pub fn reresolve(mut self, f: Reresolve) -> Self {
91 self.conn.redirect = Some(Redirect::new(f, Some(self.device.clone())));
92 self
93 }
94
95 pub async fn run(self) -> Result<(), Error> {
103 conn::run(self.driver.run(), self.conn)
104 .await
105 .map_err(Error::Driver)
106 }
107}
108
109impl PairTask {
110 pub fn reresolve(mut self, f: Reresolve) -> Self {
113 self.conn.redirect = Some(Redirect::new(f, Some(self.device.clone())));
114 self
115 }
116
117 pub async fn run(self) -> Result<(), Error> {
120 conn::run(self.driver.run(), self.conn)
121 .await
122 .map_err(Error::Driver)
123 }
124}
125
126pub(crate) fn configure(mqtt: &mut MqttOptions, will: &LastWill, max_description: usize) {
130 mqtt.set_last_will(rumqttc::LastWill::new(
131 will.topic.clone(),
132 will.payload.clone(),
133 conn::qos(will.qos),
134 will.retain,
135 ));
136 mqtt.set_clean_session(true);
137 let inflight = mqtt.inflight().min(MAX_INFLIGHT);
138 mqtt.set_inflight(inflight);
139 let incoming = mqtt.max_packet_size();
140 mqtt.set_max_packet_size(incoming, max_description.saturating_add(PACKET_OVERHEAD));
141}
142
143fn options(tree: &mut TreeBuilder, mut opts: DeviceOptions) -> DeviceOptions {
144 tree.set_receipts(true);
145 opts.link_up_discards_queued = true;
146 opts
147}
148
149pub fn device(
157 mut mqtt: MqttOptions,
158 mut tree: TreeBuilder,
159 opts: DeviceOptions,
160) -> Result<RunningDevice, BuildError> {
161 let opts = options(&mut tree, opts);
162 let max = tree.config().max_description_bytes;
163 let a = attach_device(tree, opts)?;
164 configure(&mut mqtt, &a.last_will, max);
165 let (client, eventloop) = AsyncClient::new(mqtt, REQUEST_CAPACITY);
166 Ok(RunningDevice {
167 handle: a.handle,
168 task: DeviceTask {
169 driver: a.driver,
170 conn: Conn {
171 inbox: a.inbox,
172 commands: a.commands,
173 client,
174 eventloop,
175 receipts: true,
176 flush_timeout: Duration::from_millis(opts.flush_timeout_ms),
177 redirect: None,
178 },
179 device: DeviceConf {
180 will: a.last_will,
181 max_description: max,
182 },
183 },
184 })
185}
186
187pub fn pair(
191 mut mqtt: MqttOptions,
192 mut tree: TreeBuilder,
193 config: Config,
194 opts: DeviceOptions,
195) -> Result<RunningPair, AttachError> {
196 let opts = options(&mut tree, opts);
197 let max = tree.config().max_description_bytes;
198 let a = attach_pair(tree, config, opts)?;
199 configure(&mut mqtt, &a.last_will, max);
200 let (client, eventloop) = AsyncClient::new(mqtt, REQUEST_CAPACITY);
201 Ok(RunningPair {
202 device: a.device,
203 controller: a.controller,
204 events: a.events,
205 task: PairTask {
206 driver: a.driver,
207 conn: Conn {
208 inbox: a.inbox,
209 commands: a.commands,
210 client,
211 eventloop,
212 receipts: true,
213 flush_timeout: Duration::from_millis(opts.flush_timeout_ms),
214 redirect: None,
215 },
216 device: DeviceConf {
217 will: a.last_will,
218 max_description: max,
219 },
220 },
221 })
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 use ebus_homie_tokio::device::{DeviceSpec, TreeConfig};
229
230 fn send_check<T: Send>(_: &T) {}
231
232 fn tree() -> TreeBuilder {
233 TreeBuilder::new(TreeConfig::default(), "r", DeviceSpec::default()).expect("tree")
234 }
235
236 #[test]
237 fn device_connections_carry_the_will_and_the_limits() {
238 let mut mqtt = MqttOptions::new("r", "localhost", 1883);
239 mqtt.set_clean_session(false);
240 let will = LastWill::new(
241 "ebus/5/r/$state",
242 b"lost".to_vec(),
243 ebus_homie_tokio::ebus_mqtt::QoS::ExactlyOnce,
244 true,
245 );
246 configure(&mut mqtt, &will, 10_000);
247 let w = mqtt.last_will().expect("will");
248 assert_eq!(w.topic, "ebus/5/r/$state");
249 assert_eq!(&w.message[..], b"lost");
250 assert_eq!(w.qos, rumqttc::QoS::ExactlyOnce);
251 assert!(w.retain);
252 assert!(mqtt.clean_session());
253 assert_eq!(mqtt.inflight(), 20);
254 mqtt.set_inflight(5);
255 configure(&mut mqtt, &will, 10_000);
256 assert_eq!(mqtt.inflight(), 5, "a lower cap is kept");
257 }
258
259 async fn publish_reaches_broker(device: bool, payload: usize) -> bool {
263 use tokio::io::{AsyncReadExt, AsyncWriteExt};
264 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
265 .await
266 .expect("bind");
267 let port = listener.local_addr().expect("address").port();
268 let broker = tokio::spawn(async move {
269 let (mut s, _) = listener.accept().await.expect("accept");
270 let mut buf = vec![0u8; 4096];
271 let _ = s.read(&mut buf).await;
272 s.write_all(&[0x20, 2, 0, 0]).await.expect("CONNACK");
273 let mut total = 0;
274 while total < payload {
275 match s.read(&mut buf).await {
276 Ok(0) | Err(_) => return false,
277 Ok(n) => total += n,
278 }
279 }
280 true
281 });
282 let mut mqtt = MqttOptions::new("r", "127.0.0.1", port);
283 if device {
284 let will = LastWill::new(
285 "ebus/5/r/$state",
286 b"lost".to_vec(),
287 ebus_homie_tokio::ebus_mqtt::QoS::AtLeastOnce,
288 true,
289 );
290 configure(&mut mqtt, &will, payload);
291 }
292 let (client, mut eventloop) = AsyncClient::new(mqtt, 4);
293 client
294 .publish("t", rumqttc::QoS::AtMostOnce, false, vec![b'x'; payload])
295 .await
296 .expect("queued");
297 let poller = tokio::spawn(async move { while eventloop.poll().await.is_ok() {} });
298 let reached = tokio::time::timeout(Duration::from_secs(5), broker)
299 .await
300 .is_ok_and(|r| r.expect("broker task"));
301 poller.abort();
302 reached
303 }
304
305 #[tokio::test]
306 async fn device_connections_send_a_description_of_the_configured_size() {
307 assert!(!publish_reaches_broker(false, 12_000).await);
309 assert!(publish_reaches_broker(true, 12_000).await);
310 }
311
312 #[test]
313 fn tasks_are_send() {
314 let mqtt = MqttOptions::new("r", "localhost", 1883);
315 let run = device(mqtt.clone(), tree(), DeviceOptions::default()).expect("device");
316 send_check(&run.task.run());
317 let run = device(mqtt.clone(), tree(), DeviceOptions::default()).expect("device");
318 send_check(
319 &run.task
320 .reresolve(Box::new(|| Box::pin(async { None })))
321 .run(),
322 );
323 let run = pair(mqtt, tree(), Config::wildcard(), DeviceOptions::default()).expect("pair");
324 send_check(
325 &run.task
326 .reresolve(Box::new(|| Box::pin(async { None })))
327 .run(),
328 );
329 }
330
331 #[tokio::test]
332 async fn abort_while_the_broker_is_down_ends_the_task() {
333 let port = std::net::TcpListener::bind("127.0.0.1:0")
334 .and_then(|l| l.local_addr())
335 .expect("free port")
336 .port();
337 let mqtt = MqttOptions::new("r", "127.0.0.1", port);
338 let run = device(mqtt, tree(), DeviceOptions::default()).expect("device");
339 let task = tokio::spawn(run.task.run());
340 tokio::time::sleep(Duration::from_millis(100)).await;
341 run.handle.abort();
342 let ended = tokio::time::timeout(Duration::from_secs(10), task).await;
343 assert!(matches!(ended, Ok(Ok(Ok(())))), "{ended:?}");
344 }
345
346 #[tokio::test]
347 async fn shutdown_while_the_broker_is_down_ends_the_task() {
348 let port = std::net::TcpListener::bind("127.0.0.1:0")
349 .and_then(|l| l.local_addr())
350 .expect("free port")
351 .port();
352 let mqtt = MqttOptions::new("r", "127.0.0.1", port);
353 let run = device(mqtt, tree(), DeviceOptions::default()).expect("device");
354 let task = tokio::spawn(run.task.run());
355 tokio::time::sleep(Duration::from_millis(100)).await;
356 run.handle
357 .shutdown(ebus_homie_tokio::device::Shutdown::Announce)
358 .await;
359 let ended = tokio::time::timeout(Duration::from_secs(10), task).await;
360 assert!(matches!(ended, Ok(Ok(Ok(())))), "{ended:?}");
361 }
362}