This commit is contained in:
parent
25987475dd
commit
9e8385d74d
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
mod model;
|
mod model;
|
||||||
mod mqtt_interface;
|
mod mqtt;
|
||||||
mod option_future;
|
mod option_future;
|
||||||
mod program_runner;
|
mod program_runner;
|
||||||
mod schedule;
|
mod schedule;
|
||||||
@ -57,13 +57,13 @@ async fn main() -> Result<()> {
|
|||||||
debug!(program = debug(&prog), "read program");
|
debug!(program = debug(&prog), "read program");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mqtt_options = mqtt_interface::Options {
|
let mqtt_options = mqtt::Options {
|
||||||
broker_host: "localhost".into(),
|
broker_host: "localhost".into(),
|
||||||
broker_port: 1883,
|
broker_port: 1883,
|
||||||
device_id: "sprinklers_rs-0001".into(),
|
device_id: "sprinklers_rs-0001".into(),
|
||||||
client_id: "sprinklers_rs-0001".into(),
|
client_id: "sprinklers_rs-0001".into(),
|
||||||
};
|
};
|
||||||
let mut mqtt_interface = mqtt_interface::MqttInterfaceTask::start(mqtt_options);
|
let mut mqtt_interface = mqtt::MqttInterfaceTask::start(mqtt_options);
|
||||||
|
|
||||||
let update_listener = {
|
let update_listener = {
|
||||||
let section_events = section_runner.subscribe().await?;
|
let section_events = section_runner.subscribe().await?;
|
||||||
|
96
src/mqtt/actor.rs
Normal file
96
src/mqtt/actor.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use super::{event_loop::EventLoopTask, MqttInterface};
|
||||||
|
use actix::{Actor, ActorContext, ActorFuture, AsyncContext, Handler, WrapFuture};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
use tracing::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
|
pub(super) struct MqttActor {
|
||||||
|
interface: MqttInterface,
|
||||||
|
event_loop: Option<rumqttc::EventLoop>,
|
||||||
|
quit_rx: Option<oneshot::Receiver<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttActor {
|
||||||
|
pub(super) 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 = "()")]
|
||||||
|
pub(super) 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 = "()")]
|
||||||
|
pub(super) 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 = "()")]
|
||||||
|
pub(super) struct PubRecieve(pub(super) rumqttc::Publish);
|
||||||
|
|
||||||
|
impl Handler<PubRecieve> for MqttActor {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: PubRecieve, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
let topic = &msg.0.topic;
|
||||||
|
if topic == &self.interface.topics.requests() {
|
||||||
|
debug!("received request: {:?}", msg.0);
|
||||||
|
} else {
|
||||||
|
warn!("received on unknown topic: {:?}", topic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
src/mqtt/event_loop.rs
Normal file
82
src/mqtt/event_loop.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
use super::actor::{self, MqttActor};
|
||||||
|
use actix::Addr;
|
||||||
|
use rumqttc::{Packet, QoS};
|
||||||
|
use std::{collections::HashSet, time::Duration};
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
use tracing::{debug, trace, warn};
|
||||||
|
|
||||||
|
pub(super) struct EventLoopTask {
|
||||||
|
event_loop: rumqttc::EventLoop,
|
||||||
|
mqtt_addr: Addr<MqttActor>,
|
||||||
|
quit_tx: oneshot::Sender<()>,
|
||||||
|
unreleased_pubs: HashSet<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventLoopTask {
|
||||||
|
pub(super) 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(actor::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(actor::PubRecieve(publish));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Packet::PubRel(pubrel) => {
|
||||||
|
self.unreleased_pubs.remove(&pubrel.pkid);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) 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(());
|
||||||
|
}
|
||||||
|
}
|
173
src/mqtt/mod.rs
Normal file
173
src/mqtt/mod.rs
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
mod actor;
|
||||||
|
mod event_loop;
|
||||||
|
mod topics;
|
||||||
|
|
||||||
|
use self::topics::Topics;
|
||||||
|
use crate::{
|
||||||
|
model::{Program, ProgramId, Programs, Section, SectionId, Sections},
|
||||||
|
section_runner::SecRunnerState,
|
||||||
|
section_runner_json::SecRunnerStateJson,
|
||||||
|
};
|
||||||
|
use actix::{Actor, Addr};
|
||||||
|
use eyre::WrapErr;
|
||||||
|
use rumqttc::{LastWill, MqttOptions, QoS};
|
||||||
|
use std::{
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Options {
|
||||||
|
pub broker_host: String,
|
||||||
|
pub broker_port: u16,
|
||||||
|
pub device_id: String,
|
||||||
|
pub client_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MqttInterface {
|
||||||
|
client: rumqttc::AsyncClient,
|
||||||
|
topics: Topics<Arc<str>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttInterface {
|
||||||
|
fn new(options: Options) -> (Self, rumqttc::EventLoop) {
|
||||||
|
let mqtt_prefix = format!("devices/{}", options.device_id);
|
||||||
|
let topics: Topics<Arc<str>> = Topics::new(mqtt_prefix.into());
|
||||||
|
let mut mqtt_opts =
|
||||||
|
MqttOptions::new(options.client_id, options.broker_host, options.broker_port);
|
||||||
|
|
||||||
|
let last_will = LastWill::new(topics.connected(), "false", QoS::AtLeastOnce, true);
|
||||||
|
mqtt_opts.set_last_will(last_will);
|
||||||
|
|
||||||
|
let (client, event_loop) = rumqttc::AsyncClient::new(mqtt_opts, 16);
|
||||||
|
|
||||||
|
(Self { client, topics }, event_loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn publish_data<P>(&mut self, topic: String, payload: &P) -> eyre::Result<()>
|
||||||
|
where
|
||||||
|
P: serde::Serialize,
|
||||||
|
{
|
||||||
|
let payload_vec =
|
||||||
|
serde_json::to_vec(payload).wrap_err("failed to serialize publish payload")?;
|
||||||
|
self.client
|
||||||
|
.publish(topic, QoS::AtLeastOnce, true, payload_vec)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to publish")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn publish_connected(&mut self, connected: bool) -> eyre::Result<()> {
|
||||||
|
self.publish_data(self.topics.connected(), &connected)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to publish connected topic")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn cancel(&mut self) -> Result<(), rumqttc::ClientError> {
|
||||||
|
self.client.cancel().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_sections(&mut self, sections: &Sections) -> eyre::Result<()> {
|
||||||
|
let section_ids: Vec<_> = sections.keys().cloned().collect();
|
||||||
|
self.publish_data(self.topics.sections(), §ion_ids)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to publish section ids")?;
|
||||||
|
for section in sections.values() {
|
||||||
|
self.publish_section(section).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_section(&mut self, section: &Section) -> eyre::Result<()> {
|
||||||
|
self.publish_data(self.topics.section_data(section.id), section)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to publish section")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section state can be derived from section runner state...
|
||||||
|
pub async fn publish_section_state(
|
||||||
|
&mut self,
|
||||||
|
section_id: SectionId,
|
||||||
|
state: bool,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
self.publish_data(self.topics.section_state(section_id), &state)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to publish section state")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_programs(&mut self, programs: &Programs) -> eyre::Result<()> {
|
||||||
|
let program_ids: Vec<_> = programs.keys().cloned().collect();
|
||||||
|
self.publish_data(self.topics.programs(), &program_ids)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to publish program ids")?;
|
||||||
|
for program in programs.values() {
|
||||||
|
self.publish_program(program).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_program(&mut self, program: &Program) -> eyre::Result<()> {
|
||||||
|
self.publish_data(self.topics.program_data(program.id), &program)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to publish program")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_program_running(
|
||||||
|
&mut self,
|
||||||
|
program_id: ProgramId,
|
||||||
|
running: bool,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
self.publish_data(self.topics.program_running(program_id), &running)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to publish program running")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_section_runner(&mut self, sr_state: &SecRunnerState) -> eyre::Result<()> {
|
||||||
|
let json: SecRunnerStateJson = sr_state.into();
|
||||||
|
self.publish_data(self.topics.section_runner(), &json)
|
||||||
|
.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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MqttInterfaceTask {
|
||||||
|
interface: MqttInterface,
|
||||||
|
addr: Addr<actor::MqttActor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttInterfaceTask {
|
||||||
|
pub fn start(options: Options) -> Self {
|
||||||
|
let (interface, event_loop) = MqttInterface::new(options);
|
||||||
|
|
||||||
|
let addr = actor::MqttActor::new(interface.clone(), event_loop).start();
|
||||||
|
|
||||||
|
Self { interface, addr }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn quit(self) -> eyre::Result<()> {
|
||||||
|
self.addr.send(actor::Quit).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for MqttInterfaceTask {
|
||||||
|
type Target = MqttInterface;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.interface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for MqttInterfaceTask {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.interface
|
||||||
|
}
|
||||||
|
}
|
58
src/mqtt/topics.rs
Normal file
58
src/mqtt/topics.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use crate::model::{ProgramId, SectionId};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Topics<T>
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
prefix: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Topics<T>
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
pub fn new(prefix: T) -> Self {
|
||||||
|
Self { prefix }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connected(&self) -> String {
|
||||||
|
format!("{}/connected", self.prefix.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sections(&self) -> String {
|
||||||
|
format!("{}/sections", self.prefix.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_data(&self, section_id: SectionId) -> String {
|
||||||
|
format!("{}/sections/{}", self.prefix.as_ref(), section_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_state(&self, section_id: SectionId) -> String {
|
||||||
|
format!("{}/sections/{}/state", self.prefix.as_ref(), section_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn programs(&self) -> String {
|
||||||
|
format!("{}/programs", self.prefix.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn program_data(&self, program_id: ProgramId) -> String {
|
||||||
|
format!("{}/programs/{}", self.prefix.as_ref(), program_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn program_running(&self, program_id: ProgramId) -> String {
|
||||||
|
format!("{}/programs/{}/running", self.prefix.as_ref(), program_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section_runner(&self) -> String {
|
||||||
|
format!("{}/section_runner", self.prefix.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn requests(&self) -> String {
|
||||||
|
format!("{}/requests", self.prefix.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn responses(&self) -> String {
|
||||||
|
format!("{}/responses", self.prefix.as_ref())
|
||||||
|
}
|
||||||
|
}
|
@ -1,392 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
model::{Program, ProgramId, Programs, Section, SectionId, Sections},
|
|
||||||
section_runner::SecRunnerState,
|
|
||||||
section_runner_json::SecRunnerStateJson,
|
|
||||||
};
|
|
||||||
use actix::{Actor, ActorContext, ActorFuture, Addr, AsyncContext, Handler, WrapFuture};
|
|
||||||
use eyre::WrapErr;
|
|
||||||
use rumqttc::{LastWill, MqttOptions, Packet, QoS};
|
|
||||||
use std::{
|
|
||||||
collections::HashSet,
|
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
sync::Arc,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
use tokio::sync::oneshot;
|
|
||||||
use tracing::{debug, error, info, trace, warn};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct Topics<T>
|
|
||||||
where
|
|
||||||
T: AsRef<str>,
|
|
||||||
{
|
|
||||||
prefix: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Topics<T>
|
|
||||||
where
|
|
||||||
T: AsRef<str>,
|
|
||||||
{
|
|
||||||
fn new(prefix: T) -> Self {
|
|
||||||
Self { prefix }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn connected(&self) -> String {
|
|
||||||
format!("{}/connected", self.prefix.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sections(&self) -> String {
|
|
||||||
format!("{}/sections", self.prefix.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn section_data(&self, section_id: SectionId) -> String {
|
|
||||||
format!("{}/sections/{}", self.prefix.as_ref(), section_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn section_state(&self, section_id: SectionId) -> String {
|
|
||||||
format!("{}/sections/{}/state", self.prefix.as_ref(), section_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn programs(&self) -> String {
|
|
||||||
format!("{}/programs", self.prefix.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn program_data(&self, program_id: ProgramId) -> String {
|
|
||||||
format!("{}/programs/{}", self.prefix.as_ref(), program_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn program_running(&self, program_id: ProgramId) -> String {
|
|
||||||
format!("{}/programs/{}/running", self.prefix.as_ref(), program_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
|
||||||
pub struct Options {
|
|
||||||
pub broker_host: String,
|
|
||||||
pub broker_port: u16,
|
|
||||||
pub device_id: String,
|
|
||||||
pub client_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct MqttInterface {
|
|
||||||
client: rumqttc::AsyncClient,
|
|
||||||
topics: Topics<Arc<str>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MqttInterface {
|
|
||||||
fn new(options: Options) -> (Self, rumqttc::EventLoop) {
|
|
||||||
let mqtt_prefix = format!("devices/{}", options.device_id);
|
|
||||||
let topics: Topics<Arc<str>> = Topics::new(mqtt_prefix.into());
|
|
||||||
let mut mqtt_opts =
|
|
||||||
MqttOptions::new(options.client_id, options.broker_host, options.broker_port);
|
|
||||||
|
|
||||||
let last_will = LastWill::new(topics.connected(), "false", QoS::AtLeastOnce, true);
|
|
||||||
mqtt_opts.set_last_will(last_will);
|
|
||||||
|
|
||||||
let (client, event_loop) = rumqttc::AsyncClient::new(mqtt_opts, 16);
|
|
||||||
|
|
||||||
(Self { client, topics }, event_loop)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn publish_data<P>(&mut self, topic: String, payload: &P) -> eyre::Result<()>
|
|
||||||
where
|
|
||||||
P: serde::Serialize,
|
|
||||||
{
|
|
||||||
let payload_vec =
|
|
||||||
serde_json::to_vec(payload).wrap_err("failed to serialize publish payload")?;
|
|
||||||
self.client
|
|
||||||
.publish(topic, QoS::AtLeastOnce, true, payload_vec)
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to publish")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn publish_connected(&mut self, connected: bool) -> eyre::Result<()> {
|
|
||||||
self.publish_data(self.topics.connected(), &connected)
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to publish connected topic")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn cancel(&mut self) -> Result<(), rumqttc::ClientError> {
|
|
||||||
self.client.cancel().await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn publish_sections(&mut self, sections: &Sections) -> eyre::Result<()> {
|
|
||||||
let section_ids: Vec<_> = sections.keys().cloned().collect();
|
|
||||||
self.publish_data(self.topics.sections(), §ion_ids)
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to publish section ids")?;
|
|
||||||
for section in sections.values() {
|
|
||||||
self.publish_section(section).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn publish_section(&mut self, section: &Section) -> eyre::Result<()> {
|
|
||||||
self.publish_data(self.topics.section_data(section.id), section)
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to publish section")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Section state can be derived from section runner state...
|
|
||||||
pub async fn publish_section_state(
|
|
||||||
&mut self,
|
|
||||||
section_id: SectionId,
|
|
||||||
state: bool,
|
|
||||||
) -> eyre::Result<()> {
|
|
||||||
self.publish_data(self.topics.section_state(section_id), &state)
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to publish section state")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn publish_programs(&mut self, programs: &Programs) -> eyre::Result<()> {
|
|
||||||
let program_ids: Vec<_> = programs.keys().cloned().collect();
|
|
||||||
self.publish_data(self.topics.programs(), &program_ids)
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to publish program ids")?;
|
|
||||||
for program in programs.values() {
|
|
||||||
self.publish_program(program).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn publish_program(&mut self, program: &Program) -> eyre::Result<()> {
|
|
||||||
self.publish_data(self.topics.program_data(program.id), &program)
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to publish program")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn publish_program_running(
|
|
||||||
&mut self,
|
|
||||||
program_id: ProgramId,
|
|
||||||
running: bool,
|
|
||||||
) -> eyre::Result<()> {
|
|
||||||
self.publish_data(self.topics.program_running(program_id), &running)
|
|
||||||
.await
|
|
||||||
.wrap_err("failed to publish program running")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn publish_section_runner(&mut self, sr_state: &SecRunnerState) -> eyre::Result<()> {
|
|
||||||
let json: SecRunnerStateJson = sr_state.into();
|
|
||||||
self.publish_data(self.topics.section_runner(), &json)
|
|
||||||
.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,
|
|
||||||
addr: Addr<MqttActor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MqttInterfaceTask {
|
|
||||||
pub fn start(options: Options) -> Self {
|
|
||||||
let (interface, event_loop) = MqttInterface::new(options);
|
|
||||||
|
|
||||||
let addr = MqttActor::new(interface.clone(), event_loop).start();
|
|
||||||
|
|
||||||
Self { interface, addr }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn quit(self) -> eyre::Result<()> {
|
|
||||||
self.addr.send(Quit).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for MqttInterfaceTask {
|
|
||||||
type Target = MqttInterface;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.interface
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for MqttInterfaceTask {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.interface
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
mqtt_interface::MqttInterface,
|
mqtt::MqttInterface,
|
||||||
program_runner::{ProgramEvent, ProgramEventRecv},
|
program_runner::{ProgramEvent, ProgramEventRecv},
|
||||||
section_runner::{SecRunnerState, SecRunnerStateRecv, SectionEvent, SectionEventRecv},
|
section_runner::{SecRunnerState, SecRunnerStateRecv, SectionEvent, SectionEventRecv},
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user