Add support for publishing sections
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
12c326ad86
commit
2270c69f2b
@ -21,6 +21,10 @@ assert_matches = "1.3.0"
|
|||||||
serde = { version = "1.0.116", features = ["derive"] }
|
serde = { version = "1.0.116", features = ["derive"] }
|
||||||
serde_json = "1.0.57"
|
serde_json = "1.0.57"
|
||||||
|
|
||||||
|
[dependencies.rumqttc]
|
||||||
|
git = "https://github.com/bytebeamio/rumqtt.git"
|
||||||
|
rev = "814dc891"
|
||||||
|
|
||||||
[dependencies.tracing-subscriber]
|
[dependencies.tracing-subscriber]
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
11
src/main.rs
11
src/main.rs
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
mod model;
|
mod model;
|
||||||
|
mod mqtt_interface;
|
||||||
mod option_future;
|
mod option_future;
|
||||||
mod program_runner;
|
mod program_runner;
|
||||||
mod schedule;
|
mod schedule;
|
||||||
@ -53,7 +54,16 @@ async fn main() -> Result<()> {
|
|||||||
info!(program = debug(&prog), "read program");
|
info!(program = debug(&prog), "read program");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mqtt_options = mqtt_interface::Options {
|
||||||
|
broker_host: "localhost".into(),
|
||||||
|
broker_port: 1883,
|
||||||
|
device_id: "sprinklers_rs-0001".into(),
|
||||||
|
client_id: "sprinklers_rs-0001".into(),
|
||||||
|
};
|
||||||
|
let mut mqtt_interface = mqtt_interface::MqttInterfaceTask::start(mqtt_options).await?;
|
||||||
|
|
||||||
program_runner.update_sections(sections.clone()).await?;
|
program_runner.update_sections(sections.clone()).await?;
|
||||||
|
mqtt_interface.publish_sections(§ions).await?;
|
||||||
program_runner.update_programs(programs.clone()).await?;
|
program_runner.update_programs(programs.clone()).await?;
|
||||||
|
|
||||||
info!("sprinklers_rs initialized");
|
info!("sprinklers_rs initialized");
|
||||||
@ -61,6 +71,7 @@ async fn main() -> Result<()> {
|
|||||||
tokio::signal::ctrl_c().await?;
|
tokio::signal::ctrl_c().await?;
|
||||||
info!("Interrupt received, shutting down");
|
info!("Interrupt received, shutting down");
|
||||||
|
|
||||||
|
mqtt_interface.quit().await?;
|
||||||
program_runner.quit().await?;
|
program_runner.quit().await?;
|
||||||
section_runner.quit().await?;
|
section_runner.quit().await?;
|
||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
|
@ -37,7 +37,8 @@ pub type ProgramSequence = Vec<ProgramItem>;
|
|||||||
|
|
||||||
pub type ProgramId = u32;
|
pub type ProgramId = u32;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Program {
|
pub struct Program {
|
||||||
pub id: ProgramId,
|
pub id: ProgramId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
//! Data models for sprinklers sections
|
//! Data models for sprinklers sections
|
||||||
//!
|
//!
|
||||||
//! A section represents a group of sprinkler heads actuated by a single
|
//! A section represents a group of sprinkler heads actuated by a single
|
||||||
//! valve. Physically controllable (or virtual) valves are handled by implementations of
|
//! valve. Physically controllable (or virtual) valves are handled by implementations of
|
||||||
//! [SectionInterface](../../section_interface/trait.SectionInterface.html), but the model
|
//! [SectionInterface](../../section_interface/trait.SectionInterface.html), but the model
|
||||||
//! describes a logical section and how it maps to a physical one.
|
//! describes a logical section and how it maps to a physical one.
|
||||||
|
|
||||||
use crate::section_interface::SecId;
|
use crate::section_interface::SecId;
|
||||||
use rusqlite::{Error as SqlError, Row as SqlRow, ToSql};
|
use rusqlite::{Error as SqlError, Row as SqlRow, ToSql};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Identifying integer type for a Section
|
/// Identifying integer type for a Section
|
||||||
pub type SectionId = u32;
|
pub type SectionId = u32;
|
||||||
|
|
||||||
/// A single logical section
|
/// A single logical section
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Section {
|
pub struct Section {
|
||||||
pub id: SectionId,
|
pub id: SectionId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
199
src/mqtt_interface.rs
Normal file
199
src/mqtt_interface.rs
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
use crate::model::{Section, SectionId, Sections};
|
||||||
|
use eyre::WrapErr;
|
||||||
|
use rumqttc::{LastWill, MqttOptions, QoS};
|
||||||
|
use std::{
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tokio::{task::JoinHandle, time::delay_for};
|
||||||
|
use tracing::{debug, trace, warn, info};
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Options {
|
||||||
|
pub broker_host: String,
|
||||||
|
pub broker_port: u16,
|
||||||
|
pub device_id: String,
|
||||||
|
pub client_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn event_loop_task(
|
||||||
|
mut interface: MqttInterface,
|
||||||
|
mut event_loop: rumqttc::EventLoop,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
use rumqttc::{ConnectionError, Event};
|
||||||
|
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");
|
||||||
|
interface.publish_connected(true).await?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Outgoing(outgoing)) => {
|
||||||
|
trace!(outgoing = debug(&outgoing), "MQTT outgoing message");
|
||||||
|
}
|
||||||
|
Err(ConnectionError::Cancel) => {
|
||||||
|
debug!("MQTT disconnecting");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let reconnect_timeout = Duration::from_secs(5);
|
||||||
|
warn!(
|
||||||
|
"MQTT error, will reconnect in {:?}: {}",
|
||||||
|
reconnect_timeout, err
|
||||||
|
);
|
||||||
|
delay_for(reconnect_timeout).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, 32);
|
||||||
|
|
||||||
|
(Self { client, topics }, event_loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_sections(&mut self, sections: &Sections) -> eyre::Result<()> {
|
||||||
|
let section_ids: Vec<_> = sections.keys().cloned().collect();
|
||||||
|
let section_ids_payload = serde_json::to_vec(§ion_ids)?;
|
||||||
|
self.client
|
||||||
|
.publish(
|
||||||
|
self.topics.sections(),
|
||||||
|
QoS::AtLeastOnce,
|
||||||
|
true,
|
||||||
|
section_ids_payload,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
for section in sections.values() {
|
||||||
|
self.publish_section(section).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_section(&mut self, section: &Section) -> eyre::Result<()> {
|
||||||
|
let payload = serde_json::to_vec(section).wrap_err("failed to serialize section")?;
|
||||||
|
self.client
|
||||||
|
.publish(
|
||||||
|
self.topics.section_data(section.id),
|
||||||
|
QoS::AtLeastOnce,
|
||||||
|
true,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn publish_connected(&mut self, connected: bool) -> eyre::Result<()> {
|
||||||
|
self.client
|
||||||
|
.publish(
|
||||||
|
self.topics.connected(),
|
||||||
|
QoS::AtLeastOnce,
|
||||||
|
true,
|
||||||
|
connected.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to publish connected topic")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cancel(&mut self) -> Result<(), rumqttc::ClientError> {
|
||||||
|
self.client.cancel().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MqttInterfaceTask {
|
||||||
|
interface: MqttInterface,
|
||||||
|
join_handle: JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MqttInterfaceTask {
|
||||||
|
pub async fn start(options: Options) -> eyre::Result<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")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
interface,
|
||||||
|
join_handle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn quit(mut self) -> eyre::Result<()> {
|
||||||
|
self.interface.cancel().await?;
|
||||||
|
self.join_handle.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
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user