Use actix for MQTT interface
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		
							parent
							
								
									4eb03f2e22
								
							
						
					
					
						commit
						1eb1d2b058
					
				| @ -63,7 +63,7 @@ async fn main() -> Result<()> { | ||||
|         device_id: "sprinklers_rs-0001".into(), | ||||
|         client_id: "sprinklers_rs-0001".into(), | ||||
|     }; | ||||
|     let mut mqtt_interface = mqtt_interface::MqttInterfaceTask::start(mqtt_options).await?; | ||||
|     let mut mqtt_interface = mqtt_interface::MqttInterfaceTask::start(mqtt_options); | ||||
| 
 | ||||
|     let update_listener = { | ||||
|         let section_events = section_runner.subscribe().await?; | ||||
| @ -101,7 +101,7 @@ async fn main() -> Result<()> { | ||||
|     mqtt_interface.quit().await?; | ||||
|     program_runner.quit().await?; | ||||
|     section_runner.quit().await?; | ||||
|     tokio::task::yield_now().await; | ||||
|     actix::System::current().stop(); | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| @ -3,15 +3,17 @@ use crate::{ | ||||
|     section_runner::SecRunnerState, | ||||
|     section_runner_json::SecRunnerStateJson, | ||||
| }; | ||||
| use actix::{Actor, ActorContext, ActorFuture, Addr, AsyncContext, Handler, WrapFuture}; | ||||
| use eyre::WrapErr; | ||||
| use rumqttc::{LastWill, MqttOptions, QoS}; | ||||
| use rumqttc::{LastWill, MqttOptions, Packet, QoS}; | ||||
| use std::{ | ||||
|     collections::HashSet, | ||||
|     ops::{Deref, DerefMut}, | ||||
|     sync::Arc, | ||||
|     time::Duration, | ||||
| }; | ||||
| use tokio::task::JoinHandle; | ||||
| use tracing::{debug, info, trace, warn}; | ||||
| use tokio::sync::oneshot; | ||||
| use tracing::{debug, error, info, trace, warn}; | ||||
| 
 | ||||
| #[derive(Clone, Debug)] | ||||
| struct Topics<T> | ||||
| @ -60,6 +62,90 @@ where | ||||
|     fn section_runner(&self) -> String { | ||||
|         format!("{}/section_runner", self.prefix.as_ref()) | ||||
|     } | ||||
| 
 | ||||
|     fn requests(&self) -> String { | ||||
|         format!("{}/requests", self.prefix.as_ref()) | ||||
|     } | ||||
| 
 | ||||
|     fn responses(&self) -> String { | ||||
|         format!("{}/responses", self.prefix.as_ref()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| struct EventLoopTask { | ||||
|     event_loop: rumqttc::EventLoop, | ||||
|     mqtt_addr: Addr<MqttActor>, | ||||
|     quit_tx: oneshot::Sender<()>, | ||||
|     unreleased_pubs: HashSet<u16>, | ||||
| } | ||||
| 
 | ||||
| impl EventLoopTask { | ||||
|     fn new( | ||||
|         event_loop: rumqttc::EventLoop, | ||||
|         mqtt_addr: Addr<MqttActor>, | ||||
|         quit_tx: oneshot::Sender<()>, | ||||
|     ) -> Self { | ||||
|         Self { | ||||
|             event_loop, | ||||
|             mqtt_addr, | ||||
|             quit_tx, | ||||
|             unreleased_pubs: HashSet::default(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn handle_incoming(&mut self, incoming: Packet) { | ||||
|         trace!(incoming = debug(&incoming), "MQTT incoming message"); | ||||
|         #[allow(clippy::single_match)] | ||||
|         match incoming { | ||||
|             Packet::ConnAck(_) => { | ||||
|                 self.mqtt_addr.do_send(Connected); | ||||
|             } | ||||
|             Packet::Publish(publish) => { | ||||
|                 // Only deliver QoS 2 packets once
 | ||||
|                 let deliver = if publish.qos == QoS::ExactlyOnce { | ||||
|                     if self.unreleased_pubs.contains(&publish.pkid) { | ||||
|                         false | ||||
|                     } else { | ||||
|                         self.unreleased_pubs.insert(publish.pkid); | ||||
|                         true | ||||
|                     } | ||||
|                 } else { | ||||
|                     true | ||||
|                 }; | ||||
|                 if deliver { | ||||
|                     self.mqtt_addr.do_send(PubRecieve(publish)); | ||||
|                 } | ||||
|             } | ||||
|             Packet::PubRel(pubrel) => { | ||||
|                 self.unreleased_pubs.remove(&pubrel.pkid); | ||||
|             } | ||||
|             _ => {} | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async fn run(mut self) { | ||||
|         use rumqttc::{ConnectionError, Event}; | ||||
|         let reconnect_timeout = Duration::from_secs(5); | ||||
|         self.event_loop.set_reconnection_delay(reconnect_timeout); | ||||
|         loop { | ||||
|             match self.event_loop.poll().await { | ||||
|                 Ok(Event::Incoming(incoming)) => { | ||||
|                     self.handle_incoming(incoming); | ||||
|                 } | ||||
|                 Ok(Event::Outgoing(outgoing)) => { | ||||
|                     trace!(outgoing = debug(&outgoing), "MQTT outgoing message"); | ||||
|                 } | ||||
|                 Err(ConnectionError::Cancel) => { | ||||
|                     debug!("MQTT disconnecting"); | ||||
|                     break; | ||||
|                 } | ||||
|                 Err(err) => { | ||||
|                     warn!("MQTT error, reconnecting: {}", err); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         let _ = self.quit_tx.send(()); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone, Debug)] | ||||
| @ -70,48 +156,6 @@ pub struct Options { | ||||
|     pub client_id: String, | ||||
| } | ||||
| 
 | ||||
| async fn event_loop_task( | ||||
|     interface: MqttInterface, | ||||
|     mut event_loop: rumqttc::EventLoop, | ||||
| ) -> eyre::Result<()> { | ||||
|     use rumqttc::{ConnectionError, Event}; | ||||
|     let reconnect_timeout = Duration::from_secs(5); | ||||
|     event_loop.set_reconnection_delay(reconnect_timeout); | ||||
|     loop { | ||||
|         match event_loop.poll().await { | ||||
|             Ok(Event::Incoming(incoming)) => { | ||||
|                 debug!(incoming = debug(&incoming), "MQTT incoming message"); | ||||
|                 #[allow(clippy::single_match)] | ||||
|                 match incoming { | ||||
|                     rumqttc::Packet::ConnAck(_) => { | ||||
|                         info!("MQTT connected"); | ||||
|                         { | ||||
|                             // HACK: this really should just be await
 | ||||
|                             // but that can sometimes deadlock if the publish channel is full
 | ||||
|                             let mut interface = interface.clone(); | ||||
|                             let fut = async move { interface.publish_connected(true).await }; | ||||
|                             tokio::spawn(fut); | ||||
|                         } | ||||
|                         //.await?;
 | ||||
|                     } | ||||
|                     _ => {} | ||||
|                 } | ||||
|             } | ||||
|             Ok(Event::Outgoing(outgoing)) => { | ||||
|                 trace!(outgoing = debug(&outgoing), "MQTT outgoing message"); | ||||
|             } | ||||
|             Err(ConnectionError::Cancel) => { | ||||
|                 debug!("MQTT disconnecting"); | ||||
|                 break; | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 warn!("MQTT error, reconnecting: {}", err); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct MqttInterface { | ||||
|     client: rumqttc::AsyncClient, | ||||
| @ -217,35 +261,118 @@ impl MqttInterface { | ||||
|             .await | ||||
|             .wrap_err("failed to publish section runner") | ||||
|     } | ||||
| 
 | ||||
|     pub async fn subscribe_requests(&mut self) -> eyre::Result<()> { | ||||
|         self.client | ||||
|             .subscribe(self.topics.requests(), QoS::ExactlyOnce) | ||||
|             .await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| struct MqttActor { | ||||
|     interface: MqttInterface, | ||||
|     event_loop: Option<rumqttc::EventLoop>, | ||||
|     quit_rx: Option<oneshot::Receiver<()>>, | ||||
| } | ||||
| 
 | ||||
| impl MqttActor { | ||||
|     fn new(interface: MqttInterface, event_loop: rumqttc::EventLoop) -> Self { | ||||
|         Self { | ||||
|             interface, | ||||
|             event_loop: Some(event_loop), | ||||
|             quit_rx: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Actor for MqttActor { | ||||
|     type Context = actix::Context<Self>; | ||||
| 
 | ||||
|     fn started(&mut self, ctx: &mut Self::Context) { | ||||
|         trace!("MqttActor starting"); | ||||
|         let event_loop = self.event_loop.take().expect("MqttActor already started"); | ||||
|         let (quit_tx, quit_rx) = oneshot::channel(); | ||||
|         ctx.spawn( | ||||
|             EventLoopTask::new(event_loop, ctx.address(), quit_tx) | ||||
|                 .run() | ||||
|                 .into_actor(self), | ||||
|         ); | ||||
|         self.quit_rx = Some(quit_rx); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(actix::Message)] | ||||
| #[rtype(result = "()")] | ||||
| struct Quit; | ||||
| 
 | ||||
| impl Handler<Quit> for MqttActor { | ||||
|     type Result = actix::ResponseActFuture<Self, ()>; | ||||
|     fn handle(&mut self, _msg: Quit, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         let mut interface = self.interface.clone(); | ||||
|         let quit_rx = self.quit_rx.take().expect("MqttActor has already quit!"); | ||||
|         let fut = async move { | ||||
|             interface | ||||
|                 .cancel() | ||||
|                 .await | ||||
|                 .expect("could not cancel MQTT client"); | ||||
|             let _ = quit_rx.await; | ||||
|         } | ||||
|         .into_actor(self) | ||||
|         .map(|_, _, ctx| ctx.stop()); | ||||
|         Box::pin(fut) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(actix::Message)] | ||||
| #[rtype(result = "()")] | ||||
| struct Connected; | ||||
| 
 | ||||
| impl Handler<Connected> for MqttActor { | ||||
|     type Result = (); | ||||
| 
 | ||||
|     fn handle(&mut self, _msg: Connected, ctx: &mut Self::Context) -> Self::Result { | ||||
|         info!("MQTT connected"); | ||||
|         let mut interface = self.interface.clone(); | ||||
|         let fut = async move { | ||||
|             let res = interface.publish_connected(true).await; | ||||
|             let res = res.and(interface.subscribe_requests().await); | ||||
|             if let Err(err) = res { | ||||
|                 error!("error in connection setup: {}", err); | ||||
|             } | ||||
|         }; | ||||
|         ctx.spawn(fut.into_actor(self)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(actix::Message)] | ||||
| #[rtype(result = "()")] | ||||
| struct PubRecieve(rumqttc::Publish); | ||||
| 
 | ||||
| impl Handler<PubRecieve> for MqttActor { | ||||
|     type Result = (); | ||||
| 
 | ||||
|     fn handle(&mut self, msg: PubRecieve, _ctx: &mut Self::Context) -> Self::Result { | ||||
|         debug!("received MQTT pub: {:?}", msg.0); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub struct MqttInterfaceTask { | ||||
|     interface: MqttInterface, | ||||
|     join_handle: JoinHandle<()>, | ||||
|     addr: Addr<MqttActor>, | ||||
| } | ||||
| 
 | ||||
| impl MqttInterfaceTask { | ||||
|     pub async fn start(options: Options) -> eyre::Result<Self> { | ||||
|     pub fn start(options: Options) -> Self { | ||||
|         let (interface, event_loop) = MqttInterface::new(options); | ||||
| 
 | ||||
|         let join_handle = tokio::spawn({ | ||||
|             let interface = interface.clone(); | ||||
|             async move { | ||||
|                 event_loop_task(interface, event_loop) | ||||
|                     .await | ||||
|                     .expect("error in event loop task") | ||||
|             } | ||||
|         }); | ||||
|         let addr = MqttActor::new(interface.clone(), event_loop).start(); | ||||
| 
 | ||||
|         Ok(Self { | ||||
|             interface, | ||||
|             join_handle, | ||||
|         }) | ||||
|         Self { interface, addr } | ||||
|     } | ||||
| 
 | ||||
|     pub async fn quit(mut self) -> eyre::Result<()> { | ||||
|         self.interface.cancel().await?; | ||||
|         self.join_handle.await?; | ||||
|     pub async fn quit(self) -> eyre::Result<()> { | ||||
|         self.addr.send(Quit).await?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user