diff --git a/.gitignore b/.gitignore index 0728338..9dc5809 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ target # Added by cargo /target +config.secret.yml diff --git a/Cargo.lock b/Cargo.lock index 4fd5bce..1ad7296 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,13 +3,10 @@ version = 4 [[package]] -name = "aho-corasick" -version = "1.1.5" +name = "arraydeque" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "aws-lc-rs" @@ -34,12 +31,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bitflags" version = "2.13.1" @@ -102,10 +93,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "equivalent" -version = "1.0.2" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] [[package]] name = "find-msvc-tools" @@ -130,6 +124,12 @@ dependencies = [ "spin", ] +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "fs_extra" version = "1.3.0" @@ -200,17 +200,17 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] [[package]] -name = "indexmap" -version = "2.14.0" +name = "hashlink" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" dependencies = [ - "equivalent", "hashbrown", - "serde", - "serde_core", ] [[package]] @@ -244,21 +244,6 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memmap2" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" -dependencies = [ - "libc", -] - [[package]] name = "mio" version = "1.2.2" @@ -276,7 +261,7 @@ version = "0.1.0" dependencies = [ "gpio", "rumqttc", - "rust-yaml", + "yaml-rust2", ] [[package]] @@ -327,35 +312,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - [[package]] name = "ring" version = "0.17.14" @@ -391,18 +347,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "rust-yaml" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3597280f43f5f5c9b7e7f281eab078651cf3a551a439e86d9506fe3a85d74f1d" -dependencies = [ - "base64", - "indexmap", - "memmap2", - "regex", -] - [[package]] name = "rustls" version = "0.23.43" @@ -509,35 +453,6 @@ dependencies = [ "libc", ] -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "shlex" version = "2.0.1" @@ -773,6 +688,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "yaml-rust2" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6edb26322e610d4f04b7cd34478317685d24d0999437e551fb97c5441151041" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "zeroize" version = "1.9.0" diff --git a/Cargo.toml b/Cargo.toml index e4c08c1..9d1ff50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,4 @@ edition = "2024" [dependencies] gpio = "0.4.1" rumqttc = "0.25.1" -rust-yaml = "1.1.0" +yaml-rust2 = "0.12.0" diff --git a/config.schema.json b/config.schema.json index b5b2f4e..d868ddd 100644 --- a/config.schema.json +++ b/config.schema.json @@ -107,12 +107,18 @@ "mode" ] }, - "cycleInterval": { + "pollInterval": { "type": "number", "minimum": 50, "maximum": 65535, "default": 100, "description": "How many miliseconds should ellapse between polls of the pin states. Updates are only sent when the state actually changes" } - } + }, + "additionalProperties": false, + "required": [ + "pollInterval", + "topics", + "mqtt" + ] } diff --git a/config.yml b/config.yml index dd315f7..1af8811 100644 --- a/config.yml +++ b/config.yml @@ -5,3 +5,12 @@ mqtt: authentication: true password: "234" user: "mqtt" + +# TODO: Consider adding an auto-off setting for out mode +# Add default setting (also only for out) +topics: + - topic: "" + pin: 0 + mode: "in" + +pollInterval: 100 diff --git a/src/conf.rs b/src/conf.rs new file mode 100644 index 0000000..2066c5b --- /dev/null +++ b/src/conf.rs @@ -0,0 +1,124 @@ +use std::fs; +use yaml_rust2::{Yaml, YamlLoader}; + +#[derive(Debug)] +pub struct Topic { + pub topic: String, + pub pin: u16, + pub mode: PinMode, +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum PinMode { + IN, + OUT, +} + +#[derive(Debug)] +pub struct MqttConfig { + pub host: String, + pub port: u16, + pub authentication: bool, + pub user: String, + pub password: String, +} + +#[derive(Debug)] +pub struct Config { + pub mqtt: MqttConfig, + pub topics: Vec, + pub poll_interval: u16, +} + +pub fn load_config(path: &str) -> Result> { + // Read config file + let conf = fs::read_to_string(path).unwrap_or_else(|_| String::from("test")); + let yaml = &YamlLoader::load_from_str(&conf).unwrap()[0]; + let conf = yaml + .as_hash() + .expect("Invalid config found at line 1. Not an object"); + let mqtt_conf = conf + .get(&Yaml::String(String::from("mqtt"))) + .expect("MQTT config is missing.") + .as_hash() + .expect("MQTT config is invalid"); + + // Load topics and pin config + let mut topics: Vec = Vec::new(); + let mut i = 0; + for raw_topic in conf + .get(&Yaml::String(String::from("topics"))) + .expect("Topics config missing") + .as_vec() + .expect("Topics config invalid. Expected an array") + { + i += 1; + let topic = raw_topic.as_hash().expect( + &format!("Invalid topic configuration found for topic at index {}. All topics should be objects with topic, pin and mode!", i)); + topics.push( + Topic { + topic: String::from( + topic + .get(&Yaml::String(String::from("topic"))) + .expect(&format!("Invalid topic configuration found at index {}. The topic name is missing", i)) + .as_str() + .expect(&format!("Invalid topic configuration found index {}. The topic name should be a string", i)), + ), + pin: topic + .get(&Yaml::String(String::from("pin"))) + .expect(&format!("Invalid topic configuration found at index {}. A pin is required", i)) + .as_i64() + .expect(&format!("Invalid topic configuration found at index {}. The pin should be a 16 bit integer (0-65535)", i)) as u16, + mode: if topic + .get(&Yaml::String(String::from("mode"))) + .expect(&format!("Invalid topic configuration found at index {}. Mode is unset", i)) + .as_str() + .expect(&format!("Invalid topic configuration found at index {}. Mode should be a string of either 'in' or 'out'", i)) == "in" + { PinMode::IN } else { PinMode::OUT }, + } + ); + } + + // Create the config struct + Ok(Config { + mqtt: MqttConfig { + host: String::from( + mqtt_conf + .get(&Yaml::String(String::from("host"))) + .expect("Host config missing") + .as_str() + .expect("Host configuration is invalid"), + ), + port: mqtt_conf + .get(&Yaml::String(String::from("port"))) + .unwrap_or(&Yaml::Integer(1883)) + .as_i64() + .expect("Invalid port configuration. Expected integer") as u16, + authentication: mqtt_conf + .get(&Yaml::String(String::from("authentication"))) + .unwrap_or(&Yaml::Boolean(false)) + .as_bool() + .expect("Authentication configuration value incorrect"), + user: String::from( + mqtt_conf + .get(&Yaml::String(String::from("user"))) + .unwrap_or(&Yaml::String(String::from(""))) + .as_str() + .expect("User configuration invalid"), + ), + password: String::from( + mqtt_conf + .get(&Yaml::String(String::from("password"))) + .unwrap_or(&Yaml::String(String::from(""))) + .as_str() + .expect("Password configuration invalid"), + ), + }, + topics: topics, + poll_interval: conf + .get(&Yaml::String(String::from("pollInterval"))) + .expect("Missing config for pollInterval") + .as_i64() + .expect("pollInterval is not an integer value") as u16, + }) +} diff --git a/src/gpio_utils.rs b/src/gpio_utils.rs new file mode 100644 index 0000000..afcaac6 --- /dev/null +++ b/src/gpio_utils.rs @@ -0,0 +1,52 @@ +use std::collections::HashMap; + +use crate::conf::{PinMode, Topic}; + +/// Split topics into in and out pins. Returns them in this order +/// +/// * `topics`: The topics to use +pub fn split_topics_and_configure_pins(topics: Vec) -> (Vec, Vec) { + let mut in_pins: Vec = Vec::new(); + let mut out_pins: Vec = Vec::new(); + for topic in topics { + if topic.mode == PinMode::IN { + in_pins.push(topic); + } else { + out_pins.push(topic); + } + } + + return (in_pins, out_pins); +} + +pub fn read_pin_value(id: u16) {} + +pub struct GPIOController { + in_pins: HashMap, +} + +impl GPIOController { + /// Create a new GPIO controller. + /// Note that the move of ownership is intentional behaviour. Only create one instance of this controller! + /// + /// * `topics`: The topics that are going to be managed by this controller (All OUT mode pins + /// are dropped without error) + pub fn new(topics: Vec) -> Self { + let mut controller = GPIOController { + in_pins: HashMap::new(), + }; + + // Build hash maps + for topic in topics { + if topic.mode == PinMode::IN { + controller + .in_pins + .insert(String::from(topic.topic.as_str()), topic); + } + } + + return controller; + } + + pub fn handle_event(&self) {} +} diff --git a/src/main.rs b/src/main.rs index 06344dc..fc62dad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,64 +1,18 @@ -use rumqttc::{Client, MqttOptions, QoS}; -use std::{fs, thread, time::Duration}; - -struct Topic { - name: String, - pin: u16, -} +use std::{thread, time::Duration}; +mod conf; +mod gpio_utils; +mod mqtt; fn main() { + // TODO: Proper cli interface and start screen + // CLI interface should allow setting the config file println!("mqtt-remote-gpio"); - // Read config file - let conf = fs::read_to_string("config.yml").unwrap_or_else(|_| String::from("test")); - println!("{}", conf); + let config = conf::load_config("config.yml").unwrap(); - pin_setup(); + // TODO: Remove this when done + println!("{:#?}", config); + thread::sleep(Duration::from_secs(2)); - let mut mqttoptions = MqttOptions::new("mqtt", "10.0.9.60", 1883); - mqttoptions.set_keep_alive(Duration::from_secs(5)); - mqttoptions.set_credentials("mqtt", "PNS#Ka!kk8cb5uYYgXdGZkBvGPP24x"); - listener( - vec![Topic { - name: String::from("garage/test"), - pin: 0, - }], - vec![Topic { - name: String::from("garage/test"), - pin: 0, - }], - mqttoptions, - ); -} - -fn pin_setup() { - // TODO: Check that a pin is not both in and out - println!("Pin setup complete"); -} - -/// MQTT connection handler -/// -/// * `topics`: The MQTT topics to subscribe to -/// * `mqttoptions`: MQTT options for conenction -fn listener(subscribe_topics: Vec, publish_topics: Vec, mqttoptions: MqttOptions) { - let (client, mut connection) = Client::new(mqttoptions, 10); - for topic in subscribe_topics { - client.subscribe(topic.name, QoS::AtMostOnce).unwrap(); - } - - // Spawn thread to update pins - thread::spawn(move || { - loop { - for topic in &publish_topics { - client - .publish(&topic.name, QoS::AtLeastOnce, false, vec![0; 0 as usize]) - .unwrap_or_default(); - } - } - }); - - for (_, notification) in connection.iter().enumerate() { - println!("{:?}", notification); - // TODO: Handle pin value change instructions - } + mqtt::handler(config); } diff --git a/src/mqtt.rs b/src/mqtt.rs new file mode 100644 index 0000000..5ba4392 --- /dev/null +++ b/src/mqtt.rs @@ -0,0 +1,51 @@ +use crate::conf::{Config}; +use crate::gpio_utils::{GPIOController, split_topics_and_configure_pins}; +use rumqttc::{Client, MqttOptions, QoS}; +use std::{thread, time::Duration}; + +/// MQTT connection handler +/// +/// * `topics`: The MQTT topics to subscribe to +/// * `mqttoptions`: MQTT options for conenction +pub fn handler(config: Config) { + // Configure pins + let (subscribe_topics, publish_topics) = split_topics_and_configure_pins(config.topics); + + // Configure MQTT + let mut mqttoptions = MqttOptions::new("mqtt", config.mqtt.host, config.mqtt.port); + mqttoptions.set_keep_alive(Duration::from_secs(5)); + if config.mqtt.authentication { + mqttoptions.set_credentials(config.mqtt.user, config.mqtt.password); + } + let (client, mut connection) = Client::new(mqttoptions, 10); + + // Subscribe to events + for topic in subscribe_topics.iter() { + println!("Setting up topic {}", topic.topic); + client + .subscribe(topic.topic.as_str(), QoS::AtMostOnce) + .unwrap_or_else(|x| println!("Setup failed, error: {:?}", x)); + } + + // Spawn thread to monitor pins + thread::spawn(move || { + loop { + for topic in &publish_topics { + // TODO: handle fails + client + .publish(&topic.topic, QoS::AtLeastOnce, false, vec![0 as u8; 1]) + .unwrap_or_default(); + } + thread::sleep(Duration::from_millis(100)); + } + }); + + // Main thread listens to topic updates + let controller = GPIOController::new(subscribe_topics); + for (_, notification) in connection.iter().enumerate() { + println!("Notification {:?}", notification); + controller.handle_event(); + thread::sleep(Duration::from_millis(100)); + // TODO: Handle pin value change instructions + } +}