diff --git a/Cargo.lock b/Cargo.lock index 25ca567..fc60db4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,7 +56,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -308,7 +308,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -374,6 +374,26 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "gumdrop" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc700f989d2f6f0248546222d9b4258f5b02a171a431f8285a81c08142629e3" +dependencies = [ + "gumdrop_derive", +] + +[[package]] +name = "gumdrop_derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729f9bd3449d77e7831a18abfb7ba2f99ee813dfd15b8c2167c9a54ba20aa99d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -580,7 +600,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -701,7 +721,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -891,7 +911,7 @@ checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -975,6 +995,7 @@ version = "0.1.1" dependencies = [ "config", "futures", + "gumdrop", "lazy_static", "openssl", "paho-mqtt", @@ -983,6 +1004,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.48" @@ -1021,7 +1053,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] @@ -1051,7 +1083,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.48", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 22b3bfc..5b7fcd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "speeduino-to-mqtt" -version = "0.1.1" +version = "0.1.4" edition = "2021" [dependencies] @@ -11,6 +11,7 @@ futures = "0.3.30" lazy_static = "1.4.0" tokio = { version = "1", features = ["full"] } openssl = { version = "0.10.63", features = ["vendored"] } +gumdrop = "0.8.1" [dev-dependencies] -tempdir = "0.3.7" \ No newline at end of file +tempdir = "0.3.7" diff --git a/src/config.rs b/src/config.rs index dfa382a..f6ddd44 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,8 @@ -use config::Config; +use config::{Config, File}; +use std::path::Path; /// Struct to hold the application configuration. +#[derive(Clone)] pub struct AppConfig { /// The name of the serial port. pub port_name: String, @@ -19,6 +21,9 @@ pub struct AppConfig { /// Refresh rate in milliseconds. pub refresh_rate_ms: Option, + + // Optional: Path to the configuration file + pub config_path: Option, } impl Default for AppConfig { @@ -30,29 +35,51 @@ impl Default for AppConfig { mqtt_port: 1883, // Provide a default MQTT port value mqtt_base_topic: String::new(), refresh_rate_ms: Some(1000), // Set the default refresh rate to 1000ms + config_path: None } } } /// Load application configuration from a TOML file. /// -/// This function reads the configuration settings from a TOML file named "settings.toml". -/// It expects the following keys in the TOML file: "port_name", "baud_rate", "mqtt_host", "mqtt_port", and "mqtt_base_topic". +/// This function reads the configuration settings from a TOML file. /// -/// # Panics -/// Panics if any of the required configuration keys are missing or if there is an error reading the configuration file. +/// # Arguments +/// - `config_path`: An optional path to the configuration file. /// /// # Returns -/// Returns an `AppConfig` struct containing the loaded configuration. -pub fn load_configuration() -> AppConfig { - // Build a new Config object with a file source. - let config_file_path = - std::env::var("CONFIG_FILE_PATH").unwrap_or_else(|_| String::from("settings.toml")); +/// Returns a `Result` containing either the `AppConfig` struct with the loaded configuration or an error message. +pub fn load_configuration(config_path: Option<&str>) -> Result { + // Create a default configuration + let mut settings = Config::default(); + + // Try to load from the passed config_path + if let Some(path) = config_path { + match Config::builder().add_source(File::with_name(path)).build() { + Ok(config) => settings = config, + Err(err) => return Err(format!("{}", err)), + } + } else { + // Try to load from the executable's directory + if let Ok(exe_dir) = std::env::current_exe() { + let exe_dir = exe_dir.parent().unwrap_or_else(|| Path::new(".")); + let default_path = exe_dir.join("settings.toml"); + + if let Ok(config) = + Config::builder().add_source(File::with_name(default_path.to_str().unwrap())).build() + { + settings = config; + } + } - let settings = Config::builder() - .add_source(config::File::with_name(&config_file_path)) - .build() - .expect("Failed to build configuration"); + // Try to load from /etc/g86-car-telemetry/speeduino-to-mqtt.toml + if let Ok(config) = Config::builder() + .add_source(File::with_name("/usr/etc/g86-car-telemetry/speeduino-to-mqtt.toml")) + .build() + { + settings = config; + } + } // Create an AppConfig struct by extracting values from the configuration. let mut app_config = AppConfig { @@ -75,13 +102,14 @@ pub fn load_configuration() -> AppConfig { .get_int("refresh_rate_ms") .map(|value| value as u64) .ok(), + config_path: config_path.map(|p| p.to_string()), // Convert &str to String }; // If refresh_rate_ms is not specified in the config, use the default value (1000ms) if app_config.refresh_rate_ms.is_none() { app_config.refresh_rate_ms = Some(1000); } - app_config + Ok(app_config) } #[cfg(test)] diff --git a/src/ecu_serial_comms_handler.rs b/src/ecu_serial_comms_handler.rs index 5ee4cbd..59a57b6 100644 --- a/src/ecu_serial_comms_handler.rs +++ b/src/ecu_serial_comms_handler.rs @@ -11,7 +11,7 @@ use std::time::{Duration, Instant}; lazy_static! { /// Interval between commands sent to the ECU. static ref COMMAND_INTERVAL: Duration = Duration::from_millis( - load_configuration().refresh_rate_ms.unwrap_or(1000) + load_configuration(None).unwrap().refresh_rate_ms.unwrap_or(1000) ); /// Length of the engine data message. @@ -41,8 +41,8 @@ pub fn setup_serial_port(config: &AppConfig) -> Result, seri /// /// This function continuously reads data from the serial port, processes the engine data, /// and communicates with the MQTT broker based on the provided configuration. -pub fn start_ecu_communication() { - let config = load_configuration(); +pub fn start_ecu_communication(config: AppConfig) { + let arc_config = Arc::new(config); // Setup MQTT outside the loop @@ -68,8 +68,9 @@ pub fn start_ecu_communication() { // Flag to indicate whether the program should exit let should_exit = Arc::new(Mutex::new(false)); + let arc_config_thread = arc_config.clone(); + thread::spawn({ - let arc_config = arc_config.clone(); let mqtt_client = mqtt_client.clone(); let port = port.clone(); let should_exit = should_exit.clone(); @@ -87,7 +88,7 @@ pub fn start_ecu_communication() { // Process the engine data only if it's not empty if !engine_data.is_empty() { - process_speeduino_realtime_data(&engine_data, &arc_config, &mqtt_client); + process_speeduino_realtime_data(&engine_data, &arc_config_thread, &mqtt_client); // Print the connection message only if not connected if !connected { diff --git a/src/main.rs b/src/main.rs index e58a1de..ab23a1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,12 +19,31 @@ /// /// - `main()`: The main function that starts the ECU communication and displays the welcome message. /// - `displayWelcome()`: Function to display a graphical welcome message. + mod config; mod ecu_data_parser; mod ecu_serial_comms_handler; mod mqtt_handler; - +use gumdrop::Options; use ecu_serial_comms_handler::start_ecu_communication; +use crate::config::load_configuration; + +/// Define options for the program. +#[derive(Debug, Options)] +struct MyOptions { + #[options(help = "print help message")] + help: bool, + + #[options(help = "Sets a custom config file", meta = "FILE")] + config: Option, +} + +fn print_help() { + println!("Usage: gps-to-mqtt [options]"); + println!("Options:"); + println!(" -h, --help Print this help message"); + println!(" -c, --config FILE Sets a custom config file path"); +} /// Displays a graphical welcome message. fn display_welcome() { @@ -37,11 +56,32 @@ fn display_welcome() { #[tokio::main] /// The main function that starts the ECU communication and displays the welcome message. async fn main() { + + // Parse CLI arguments using gumdrop + let opts = MyOptions::parse_args_default_or_exit(); + + if opts.help { + // Use custom print_help function to display help and exit + print_help(); + std::process::exit(0); + } + // Display welcome message display_welcome(); + // Load configuration, set up serial port, and start processing + let config_path = opts.config.as_deref(); + let config = match load_configuration(config_path) { + Ok(config) => config, + Err(err) => { + // Handle the error gracefully, print a message, and exit + eprintln!("Error loading configuration: {}", err); + std::process::exit(1); + } + }; + // Start ECU communication - start_ecu_communication(); + start_ecu_communication(config.clone()); } #[cfg(test)]