Compare commits
	
		
			38 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7180acd809 | ||
|  | dc6b49dd44 | ||
|  | fbdf1ea24b | ||
| 06ce74da9f | |||
| 0acb2a2538 | |||
| bd300f163e | |||
| 25da1ac04b | |||
|  | 183764f2bb | ||
|  | ccbb23ebc7 | ||
|  | 35d65b6475 | ||
| ce7afca13c | |||
|  | 5c2f2f60ad | ||
|  | 2fcf37bfdc | ||
|  | cef76ad9aa | ||
|  | d4726a2b9a | ||
|  | a526e5fb22 | ||
|  | 0041d1c30c | ||
|  | 9d3dfda37e | ||
|  | c8d4ab3ba3 | ||
|  | c07d6db238 | ||
|  | 8e3f4f4286 | ||
|  | 922c023216 | ||
|  | 4473fa7f8b | ||
|  | f9a3451281 | ||
|  | efeb9d8227 | ||
|  | 955bc37afe | ||
|  | bdd65e740d | ||
|  | 0afbc1c508 | ||
|  | 3ecafd4534 | ||
|  | 1afa8fbe81 | ||
|  | b4a4cdd1ac | ||
|  | 5930417ae4 | ||
|  | 4b6afda278 | ||
|  | 256fdeee47 | ||
|  | c39f944b5a | ||
|  | 76fed4a4de | ||
|  | 3951b4e41c | ||
|  | e4a1788698 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,3 +4,4 @@ | ||||
| .vscode/launch.json | ||||
| .vscode/ipch | ||||
| todo.txt | ||||
| *.ini | ||||
|   | ||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							| @@ -25,3 +25,17 @@ stored in folder **/client** | ||||
| go to **/client** | ||||
| * cargo build | ||||
| * cargo run | ||||
|  | ||||
| ### Deamon | ||||
| Requires ''systemd'' | ||||
|  | ||||
| Install by creating a link to this project | ||||
| ``` | ||||
| /etc/systemd/system# ln -s /home/c3ma/led-board/client/ledBoard.service ledBoard.service | ||||
| systemctl daemon-reload | ||||
| systemctl enable ledBoard.service | ||||
| ``` | ||||
| Start deamon with | ||||
| ``` | ||||
| systemctl start ledBoard.service | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										51
									
								
								client/MQTT.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								client/MQTT.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
|  # MQTT Configuration | ||||
|  | ||||
|  This project can publish weather and public transport data to an MQTT broker. | ||||
|  To enable MQTT, follow these steps: | ||||
|  | ||||
|  ## 1. Install dependencies | ||||
|  Ensure you have Rust and Cargo installed. The MQTT support uses the Paho MQTT client crate. | ||||
|  Run: | ||||
|  ```bash | ||||
|  cargo update | ||||
|  ``` | ||||
|  | ||||
|  ## 2. Set the MQTT_BROKER environment variable | ||||
|  Before running the client, define `MQTT_BROKER` to your broker address. | ||||
|  - Without URI scheme (defaults to TCP): | ||||
|    ```bash | ||||
|    export MQTT_BROKER=localhost:1883 | ||||
|    ``` | ||||
|  - With URI scheme: | ||||
|    ```bash | ||||
|    export MQTT_BROKER=tcp://broker.example.com:1883 | ||||
|    ``` | ||||
|  | ||||
|  ## 3. Run the LED board client | ||||
|  Pass the LED board IP address as the only argument: | ||||
|  ```bash | ||||
|  export MQTT_BROKER=localhost:1883 | ||||
|  cargo run --bin ledboard_client -- 192.168.1.50 | ||||
|  ``` | ||||
|  | ||||
|  ## Topics and Payloads | ||||
|  The client publishes two topics: | ||||
|  | ||||
|  ### weather | ||||
|  JSON payload with fields: | ||||
|  - `dt`: timestamp (Unix seconds) | ||||
|  - `temp`: temperature in °C | ||||
|  - `weather`: object with `main`, `description`, `icon` | ||||
|  - `rain`: rain volume in last 3h (optional) | ||||
|  - `pop`: probability of precipitation | ||||
|  - `wind`: object with `speed`, `deg`, `gust` | ||||
|  | ||||
|  ### straba | ||||
|  JSON payload with fields: | ||||
|  - `outbound_station`: name of outbound station | ||||
|  - `outbound_diff`: seconds until outbound departure | ||||
|  - `inbound_station`: name of inbound station | ||||
|  - `inbound_diff`: seconds until inbound departure | ||||
|  | ||||
|  ## Customization | ||||
|  You can adjust MQTT topics, QoS, and message formats in `client/bin/src/main.rs` under the `publish_to_mqtt` function. | ||||
| @@ -22,3 +22,9 @@ serde_derive = "1.0" | ||||
| serde_json = "1.0" | ||||
| # end of web stuff | ||||
| ping = "0.4.1" | ||||
| paho-mqtt = "0.13.2" | ||||
| async-trait = "0.1" | ||||
| # Ini File parser | ||||
| rust-ini = "0.21" | ||||
| lazy_static = "1.4" | ||||
| futures = "0.3" | ||||
|   | ||||
							
								
								
									
										78
									
								
								client/bin/src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								client/bin/src/config.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| // config.rs | ||||
|  | ||||
| /// @file | ||||
| /// @brief Configuration settings for the application. | ||||
| /// | ||||
| /// This file defines the configuration settings that are used throughout the application. | ||||
|  | ||||
| use lazy_static::lazy_static; | ||||
| use std::sync::Mutex; | ||||
|  | ||||
| use ini::Ini; | ||||
| use std::path::Path; | ||||
|  | ||||
| const DEFAULT_REFRESH_INTERVAL: u32 = 50; | ||||
|  | ||||
|  | ||||
| // Define a struct to hold the INI configuration | ||||
| #[derive(Debug)] | ||||
| pub struct Config { | ||||
|     pub mqttPrefix: String, | ||||
|     pub mqttIPAddress: String, | ||||
|     pub panelIPAddress: String, | ||||
|     pub refreshInterval: u32 | ||||
| } | ||||
|  | ||||
| impl Config { | ||||
|     // Constructor for Config | ||||
|     pub fn new(mqtt_prefix: &str, mqtt_ip_address: &str, panel_ip_address: &str, interval: u32) -> Self { | ||||
|         Config { | ||||
|             mqttPrefix: mqtt_prefix.to_string(), | ||||
|             mqttIPAddress: mqtt_ip_address.to_string(), | ||||
|             panelIPAddress: panel_ip_address.to_string(), | ||||
|             refreshInterval: interval | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn newDefault() -> Self { | ||||
|         Config { | ||||
|             mqttPrefix: "".to_string(), | ||||
|             mqttIPAddress: "".to_string(), | ||||
|             panelIPAddress: "".to_string(), | ||||
|             refreshInterval: DEFAULT_REFRESH_INTERVAL | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Function to read the INI file | ||||
| pub fn read_ini_file(filename: String) -> Config { | ||||
|      | ||||
|     let mut config = Config::newDefault(); | ||||
|     let i = Ini::load_from_file(filename).unwrap(); | ||||
|     for (sec, prop) in i.iter() { | ||||
|          | ||||
|         for (k, v) in prop.iter() | ||||
|         { | ||||
|             println!("{:?} {}:{}", sec, k, v); | ||||
|             if (sec.is_some()) && (sec.unwrap() == "mqtt") | ||||
|             { | ||||
|                 if k == "path" | ||||
|                 { | ||||
|                     config.mqttPrefix = v.trim().to_string(); | ||||
|                 } | ||||
|                 else if k == "server" | ||||
|                 { | ||||
|                     config.mqttIPAddress = v.trim().to_string(); | ||||
|                 } | ||||
|             } | ||||
|             else if (sec.is_some()) && (sec.unwrap() == "panel") | ||||
|             { | ||||
|                 if k == "ip" | ||||
|                 { | ||||
|                     config.panelIPAddress = v.trim().to_string(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     return config; | ||||
| } | ||||
| @@ -1,10 +1,13 @@ | ||||
| use std::{time::Duration, fmt::format}; | ||||
| use std::sync::{RwLock, Arc}; | ||||
| use async_trait::async_trait; | ||||
| use paho_mqtt::{CreateOptionsBuilder, ConnectOptionsBuilder, Client ,Message as MqttMessage}; | ||||
| use str; | ||||
| use bit::BitIndex; | ||||
| use chrono_tz::Europe::Berlin; | ||||
| use chrono::{DateTime, NaiveDateTime, Utc, Timelike}; | ||||
| use std::time::{SystemTime, UNIX_EPOCH}; | ||||
| use openweathermap::forecast::Weather; | ||||
| use openweathermap::{forecast::Weather, Receiver}; | ||||
| use substring::Substring; | ||||
| use tinybmp::Bmp; | ||||
| use core::time; | ||||
| @@ -18,15 +21,23 @@ use embedded_graphics::{ | ||||
|  | ||||
| use std::net::UdpSocket; | ||||
| use std::{env, thread}; | ||||
| use std::path::Path; | ||||
| use std::io; | ||||
| use std::process::ExitCode; | ||||
|  | ||||
| use openweathermap::forecast::Forecast; | ||||
| use straba::NextDeparture; | ||||
| // This declaration will look for a file named `straba.rs` and will | ||||
| // insert its contents inside a module named `straba` under this scope | ||||
| mod straba; | ||||
|  | ||||
| use std::sync::Mutex; | ||||
| use lazy_static::lazy_static; | ||||
| use futures::executor::block_on; | ||||
| // Load INI-File handling module | ||||
| mod config; | ||||
| use config::Config; | ||||
| use crate::config::read_ini_file; | ||||
|  | ||||
| const IMAGE_SIZE_BYTE: usize = (IMAGE_WIDTH_BYTE * IMAGE_HEIGHT) as usize; /* one byte contains 8 LEDs, one in each bit */ | ||||
| const IMAGE_WIDTH: u32 = 5 * 32; | ||||
| const IMAGE_WIDTH_BYTE: u32 = IMAGE_WIDTH / 8; /* one byte contains 8 LEDs, one in each bit */ | ||||
| @@ -36,6 +47,37 @@ const IMAGE_HEIGHT_BYTE: u32 = 40; | ||||
| const IMAGE_LENGTH: usize = (IMAGE_WIDTH_BYTE * IMAGE_HEIGHT_BYTE) as usize; | ||||
| const PACKAGE_LENGTH: usize = (IMAGE_LENGTH + 1) as usize; | ||||
|  | ||||
| /// @struct MqttClient | ||||
| /// @brief A struct to hold application configuration settings. | ||||
| /// | ||||
| /// The `MqttClient` struct contains various configuration parameters that control the behavior of the application. | ||||
| pub struct MqttClient { | ||||
|     /// MQTT client option | ||||
|     pub mqtt_client: Option<paho_mqtt::Client>, | ||||
| } | ||||
|  | ||||
| impl MqttClient { | ||||
|     /// Creates a new instance of MqttClient with mqtt_client initialized to None. | ||||
|     pub fn new() -> Self { | ||||
|         MqttClient {  | ||||
|             mqtt_client: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Setter for mqtt_client | ||||
|     pub fn set_mqtt_client(&mut self, client: Option<paho_mqtt::Client>) { | ||||
|         self.mqtt_client = client; | ||||
|     } | ||||
|  | ||||
|     /// Getter for mqtt_client | ||||
|     pub fn get_mqtt_client(&self) -> &Option<paho_mqtt::Client> { | ||||
|         &self.mqtt_client | ||||
|     } | ||||
| } | ||||
|  | ||||
| lazy_static! { | ||||
|     static ref MQTTCLIENT: Mutex<MqttClient> = Mutex::new(MqttClient::new()); | ||||
| } | ||||
|  | ||||
| struct UdpDisplay { | ||||
|     image: [u8; IMAGE_SIZE_BYTE], | ||||
| @@ -243,6 +285,13 @@ fn render_clock(display: &mut UdpDisplay){ | ||||
|     .unwrap(); | ||||
| } | ||||
|  | ||||
| fn render_mqtt_message(display: &mut UdpDisplay, mqtt_message: String){ | ||||
|     let text_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On); | ||||
|     Text::new(&mqtt_message, Point::new((1) as i32, 37), text_style) | ||||
|     .draw(display) | ||||
|     .unwrap(); | ||||
| } | ||||
|  | ||||
| fn render_strab_partial(display: &mut UdpDisplay, station: &String, diff: i64, height: i32) { | ||||
|     let text_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On); | ||||
|     let mut diff_str = format!("{}min", (diff / 60)); | ||||
| @@ -276,13 +325,15 @@ fn render_strab_partial(display: &mut UdpDisplay, station: &String, diff: i64, h | ||||
| } | ||||
|  | ||||
| fn render_strab(display: &mut UdpDisplay, straba_res: &NextDeparture) { | ||||
|     render_strab_partial(display, &straba_res.outbound_station, straba_res.outbound_diff, 15); | ||||
|     render_strab_partial(display, &straba_res.inbound_station, straba_res.inbound_diff, 25); | ||||
|     render_strab_partial(display, &straba_res.outbound_station, straba_res.outbound_diff, 17); | ||||
|     render_strab_partial(display, &straba_res.inbound_station, straba_res.inbound_diff, 27); | ||||
| } | ||||
| ///////////////////////////////////////////////////////////////////////////// | ||||
|  | ||||
| fn send_package(ipaddress: String,  | ||||
|                 data: &Option<Result<Forecast, String>>, | ||||
|                 straba_res: &NextDeparture) { | ||||
|                 straba_res: &NextDeparture, | ||||
|                 mqtt_message: Option<String>) { | ||||
|     let mut package: [u8; PACKAGE_LENGTH] = [0; PACKAGE_LENGTH]; | ||||
|  | ||||
|     // Brightness | ||||
| @@ -300,14 +351,17 @@ fn send_package(ipaddress: String, | ||||
|         render_strab(&mut display, straba_res); | ||||
|     } | ||||
|  | ||||
|     if mqtt_message.is_some() { | ||||
|         render_mqtt_message(&mut display, mqtt_message.unwrap());        | ||||
|     } | ||||
|  | ||||
|     render_clock(&mut display); | ||||
|  | ||||
|  | ||||
|     package[1..PACKAGE_LENGTH].copy_from_slice(&display.image); | ||||
|     // client need to bind to client port (1 before 4242) | ||||
|     let socket = UdpSocket::bind("0.0.0.0:14242").expect("couldn't bind to address"); | ||||
|     let target = format!("{}:4242", ipaddress); | ||||
|     let socket = UdpSocket::bind("0.0.0.0:0").expect("couldn't bind to address"); | ||||
|     socket | ||||
|         .send_to(&package, ipaddress + ":4242") | ||||
|         .send_to(&package, &target) | ||||
|         .expect("couldn't send data"); | ||||
| } | ||||
|  | ||||
| @@ -318,34 +372,399 @@ LEDboardClient <ip address>" | ||||
|     ); | ||||
|     println!("one argument necessary!"); | ||||
|     println!("<ip address>"); | ||||
|     println!("second argument is optional:"); | ||||
|     println!("<ip of mqtt server>"); | ||||
|     println!(""); | ||||
|     println!("Config mode"); | ||||
|     println!("--config <file.ini>"); | ||||
| } | ||||
|  | ||||
| fn check_connection(ipaddress: String) -> bool { | ||||
|     let device_online; | ||||
|     // generate a faulty package length | ||||
|     let mut package: [u8; PACKAGE_LENGTH/2] = [0; PACKAGE_LENGTH/2]; | ||||
|     // client need to bind to client port (1 before 4242) | ||||
|     let socket = UdpSocket::bind("0.0.0.0:14242").expect("couldn't bind to address"); | ||||
|     socket.set_read_timeout(Some(Duration::from_secs(10))).unwrap(); /* 10 seconds timeout */ | ||||
|     socket | ||||
|         .send_to(&package, ipaddress + ":4242") | ||||
|         .expect("couldn't send data"); | ||||
|      | ||||
|     // Use a random local port instead of hardcoding | ||||
|     let socket = UdpSocket::bind("0.0.0.0:0").expect("couldn't bind to address"); | ||||
|     socket.set_read_timeout(Some(Duration::from_secs(10))).unwrap(); | ||||
|      | ||||
|     let target = format!("{}:4242", ipaddress); | ||||
|     match socket.send_to(&package, &target) { | ||||
|         Ok(_) => { | ||||
|             // Continue with receive | ||||
|             match socket.recv_from(&mut package) { | ||||
|                 Ok((_n, _addr)) => device_online = true, | ||||
|                 Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => device_online = false, | ||||
|                 Err(_) => device_online = false, | ||||
|             } | ||||
|         }, | ||||
|         Err(_) => device_online = false, | ||||
|     } | ||||
|      | ||||
|     device_online | ||||
| } | ||||
| /// Publishes weather and transit data to MQTT broker | ||||
| fn publish_to_mqtt(client: &Client, data: &Option<Result<Forecast, String>>, straba_res: &NextDeparture) { | ||||
|     let payload = if let Some(Ok(forecast)) = data { | ||||
|         if let Some(f) = forecast.list.first() { | ||||
|             let temp = f.main.temp; | ||||
|             let weather = f.weather.get(0).map(|w| w.main.clone()).unwrap_or_default(); | ||||
|             format!("temp:{:.1}C,weather:{}", | ||||
|                 temp, | ||||
|                 weather) | ||||
|        } else { | ||||
|             "no_forecast".to_string() | ||||
|         } | ||||
|     } else { | ||||
|         "no_data".to_string() | ||||
|     }; | ||||
|     let msg = MqttMessage::new("ledboard/forecast", payload, 1); | ||||
|     if let Err(e) = client.publish(msg) { | ||||
|         eprintln!("Error publishing MQTT message: {}", e); | ||||
|     } | ||||
|  | ||||
|     // self.recv_buff is a [u8; 8092] | ||||
|     let answer = socket.recv_from(&mut package); | ||||
|     match answer { | ||||
|         Ok((_n, _addr)) => { | ||||
|             //println!("{} bytes response from {:?} {:?}", n, addr, &package[..n]); | ||||
|             device_online = true; | ||||
|         } | ||||
|         Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { | ||||
|             device_online = false; | ||||
|         } | ||||
|         Err(_e) => { | ||||
|             device_online = false; | ||||
|     let payloadPT = { | ||||
|             format!("out:{}min,in:{}min", | ||||
|                 straba_res.outbound_diff / 60, | ||||
|                 straba_res.inbound_diff / 60) | ||||
|     }; | ||||
|     let ptmsg = MqttMessage::new("ledboard/public_transportation", payloadPT, 1); | ||||
|     if let Err(e) = client.publish(ptmsg) { | ||||
|         eprintln!("Error publishing MQTT message: {}", e); | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct Message { | ||||
|     string: Option<String> | ||||
| } | ||||
|  | ||||
| // Lazy static to load the config file content | ||||
| lazy_static! { | ||||
|     pub static ref GlobalConfiguration: Mutex<Config> = Mutex::new({ | ||||
|         Config::newDefault() | ||||
|      }); | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| /// Asynchronously publishes a message to the specified topic. | ||||
| ///  | ||||
| /// @author Gwen2.5 | ||||
| ///  | ||||
| /// # Arguments | ||||
| /// * `topic` - The MQTT topic to which the message will be published. | ||||
| /// * `message` - The message to be published. | ||||
| ///  | ||||
| /// # Returns | ||||
| /// True if the message was successfully published, false otherwise. | ||||
| fn publish_message(topic: &str, message: &str) { | ||||
|     let msg = MqttMessage::new(topic, message, 0); | ||||
|  | ||||
|     let mqtt_client =  <std::option::Option<Client> as Clone>::clone(&(MQTTCLIENT.lock().unwrap().get_mqtt_client())).unwrap(); | ||||
|     // Publish the message and ensure it completes without error | ||||
|    let result =  mqtt_client.publish(msg); | ||||
|     match result { | ||||
|         Ok(_) => (), | ||||
|         Err(error) => { | ||||
|             println!("Error publishing {error:?}"); | ||||
|         } | ||||
|     } | ||||
|     return device_online; | ||||
| } | ||||
|  | ||||
| fn main_function(parameter1: String, parameter2: Option<String>) -> ExitCode { | ||||
|  | ||||
|     // Read configuration file | ||||
|     if  (parameter1 == "--config") && (parameter2.is_some()) | ||||
|     { | ||||
|         let configOrMqttAddress: String = parameter2.unwrap(); | ||||
|         if Path::new(&configOrMqttAddress).exists() | ||||
|         { | ||||
|             let mut gc = GlobalConfiguration.lock().unwrap(); | ||||
|             let c = read_ini_file(configOrMqttAddress); | ||||
|              | ||||
|             //update configuration | ||||
|             gc.mqttIPAddress = c.mqttIPAddress; | ||||
|             gc.panelIPAddress = c.panelIPAddress; | ||||
|             gc.mqttPrefix = c.mqttPrefix; | ||||
|             println!("Read INI {:} @ {:}", gc.mqttPrefix, gc.mqttIPAddress); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             /* Panel and MQTT Configured*/ | ||||
|             println!("INI file not found"); | ||||
|             return ExitCode::FAILURE; | ||||
|         } | ||||
|     } | ||||
|     else | ||||
|     { | ||||
|         let mut gc = GlobalConfiguration.lock().unwrap();    | ||||
|         gc.panelIPAddress = parameter1; | ||||
|         gc.mqttIPAddress = parameter2.unwrap(); | ||||
|     } | ||||
|  | ||||
|     let mut device_online = false; | ||||
|     if GlobalConfiguration.lock().unwrap().panelIPAddress.len() > 0 | ||||
|     { | ||||
|         device_online = check_connection(GlobalConfiguration.lock().unwrap().panelIPAddress.clone()); | ||||
|         if !device_online { | ||||
|             println!("{:} not online", &GlobalConfiguration.lock().unwrap().panelIPAddress); | ||||
|             return ExitCode::FAILURE; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let mut gmc = MQTTCLIENT.lock().unwrap(); | ||||
|     let mqtt_message: Arc<Mutex<Message>> = Arc::new(Mutex::new(Message{ string: None })); | ||||
|  | ||||
|     if GlobalConfiguration.lock().is_ok() && (GlobalConfiguration.lock().unwrap().mqttIPAddress.len() > 0) | ||||
|     { | ||||
|         // Define the set of options for the create. | ||||
|         // Use an ID for a persistent session. | ||||
|         let create_opts = paho_mqtt::CreateOptionsBuilder::new() | ||||
|         .server_uri(GlobalConfiguration.lock().unwrap().mqttIPAddress.clone()) | ||||
|         .client_id("ledboard") | ||||
|         .finalize(); | ||||
|         // Create a client. | ||||
|         let local_mqtt = paho_mqtt::Client::new(create_opts).unwrap(); | ||||
|          | ||||
|         println!("MQTT | Connecting to {:} MQTT server...", GlobalConfiguration.lock().unwrap().mqttIPAddress); | ||||
|          | ||||
|         let lwt = paho_mqtt::Message::new(&format!("{}/lwt", GlobalConfiguration.lock().unwrap().mqttPrefix), "lost connection", 1); | ||||
|  | ||||
|         // The connect options. Defaults to an MQTT v3.x connection. | ||||
|         let conn_opts = paho_mqtt::ConnectOptionsBuilder::new() | ||||
|             .keep_alive_interval(Duration::from_secs(20)) | ||||
|             .will_message(lwt) | ||||
|             .finalize(); | ||||
|  | ||||
|         // Make the connection to the broker | ||||
|         println!("MQTT | Connecting to the MQTT server..."); | ||||
|         let result = local_mqtt.connect(conn_opts); | ||||
|  | ||||
|         match result { | ||||
|             Ok(_) => { | ||||
|                 println!("MQTT | Server connected"); | ||||
|             }, | ||||
|             Err(error) => { | ||||
|                 println!("MQTT | Server connecting {error:?}"); | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|         // Subscribe to the desired topic(s). | ||||
|         local_mqtt.subscribe("room/ledboard", paho_mqtt::QOS_0); | ||||
|  | ||||
|         // Starts the client receiving messages | ||||
|         let rx_queue = local_mqtt.start_consuming(); | ||||
|  | ||||
|         // Attach a closure to the client to receive callback | ||||
|         // on incoming messages. | ||||
|         let mqtt_message_for_callback = mqtt_message.clone(); | ||||
|          | ||||
|         // Create a thread that stays pending over incoming messages. | ||||
|         let handle = thread::spawn(move || { | ||||
|             for mqttmsg in rx_queue.iter() { | ||||
|                 if let Some(mqttmsg) = mqttmsg { | ||||
|                     let topic = mqttmsg.topic(); | ||||
|                     let payload_str = mqttmsg.payload_str(); | ||||
|  | ||||
|                     //println!("MQTT | {} - {}", topic, payload_str); | ||||
|                     let mut lock = mqtt_message_for_callback.lock().unwrap(); | ||||
|                     lock.string = Some(payload_str.to_string()); | ||||
|                     println!("Received: -> {}", mqttmsg.payload_str()); | ||||
|                 } else { | ||||
|                     println!("Unsubscribe: connection closed"); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|         // Define the set of options for the connection | ||||
|  | ||||
|         // move local instance to global scope | ||||
|         gmc.set_mqtt_client(Some(local_mqtt)); | ||||
|     } | ||||
|  | ||||
|     let receiver = openweathermap::init_forecast("Mannheim", | ||||
|     "metric", | ||||
|     "de", | ||||
|     "978882ab9dd05e7122ff2b0aef2d3e55", | ||||
|     60,1); | ||||
|  | ||||
|     let mut last_data = Option::None; | ||||
|      | ||||
|     // Test Webcrawler for public transportataion | ||||
|     let mut straba_res = straba::fetch_data(Some(true)); | ||||
|     println!("{:?} {:?}s", straba_res.outbound_station, straba_res.outbound_diff); | ||||
|     println!("{:?} {:?}s", straba_res.inbound_station , straba_res.inbound_diff); | ||||
|  | ||||
|     if GlobalConfiguration.lock().is_ok() && (GlobalConfiguration.lock().unwrap().panelIPAddress.len() > 0) | ||||
|     { | ||||
|         // Render start | ||||
|         send_package(GlobalConfiguration.lock().unwrap().panelIPAddress.clone(), &last_data, &straba_res, Some("MQTT: room/ledboard".to_string())); | ||||
|     } | ||||
|  | ||||
|     loop { | ||||
|         let st_now = SystemTime::now(); | ||||
|         let seconds = st_now.duration_since(UNIX_EPOCH).unwrap().as_secs(); | ||||
|         let delay = time::Duration::from_millis(500); | ||||
|         thread::sleep(delay); | ||||
|         // Only request, if the device is present | ||||
|         if device_online == true { | ||||
|             let answer = openweathermap::update_forecast(&receiver); | ||||
|             match answer { | ||||
|                 Some(_) => { | ||||
|                     last_data = answer; | ||||
|                 } | ||||
|                 None => { | ||||
|      | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (straba_res.request_time + (GlobalConfiguration.lock().unwrap().refreshInterval as i64)) < seconds as i64 { | ||||
|             if GlobalConfiguration.lock().is_ok() && GlobalConfiguration.lock().unwrap().mqttIPAddress.len() > 0 | ||||
|             { | ||||
|                 device_online = check_connection(GlobalConfiguration.lock().unwrap().mqttIPAddress.clone()); | ||||
|             } | ||||
|  | ||||
|             if GlobalConfiguration.lock().is_ok() && GlobalConfiguration.lock().unwrap().mqttPrefix.len() > 0 | ||||
|             { | ||||
|                 //FIXME if mqtt_client.is_some() | ||||
|                 fun_publishinfoviamqtt(&straba_res); | ||||
|             } | ||||
|              | ||||
|             // request once a minute new data | ||||
|             straba_res = straba::fetch_data(None); | ||||
|             println!("Update {:?} {:?}s", straba_res.outbound_station, straba_res.outbound_diff); | ||||
|             println!("Update {:?} {:?}s", straba_res.inbound_station , straba_res.inbound_diff); | ||||
|         } | ||||
|  | ||||
|         let lock = mqtt_message.lock().unwrap(); | ||||
|         let mqtt_message: Option<String>; | ||||
|         if lock.string.is_some() { | ||||
|             mqtt_message = lock.string.clone(); | ||||
|         } else { | ||||
|             mqtt_message = None; | ||||
|         } | ||||
|  | ||||
|         if (GlobalConfiguration.lock().is_ok()) && (GlobalConfiguration.lock().unwrap().mqttIPAddress.len() > 0) && (device_online == true) { | ||||
|             // Render new image | ||||
|             send_package(GlobalConfiguration.lock().unwrap().mqttIPAddress.clone(), &last_data, &straba_res, mqtt_message); | ||||
|         } | ||||
|         // Handle MQTT messages | ||||
|          | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn fun_publishinfoviamqtt(straba_res: &NextDeparture) { | ||||
|     let topic_in_station: String = format!("{}{}", GlobalConfiguration.lock().unwrap().mqttPrefix, "/inbound/station"); | ||||
|     let station_name: String = format!("{}", straba_res.inbound_station); | ||||
|     // Execute async publish synchronously | ||||
|     let _ = publish_message(topic_in_station.as_str(), station_name.as_str()); | ||||
|     println!("MQTT published {:?} = {:?}s", topic_in_station, straba_res.outbound_station); | ||||
| } | ||||
|  | ||||
| fn main_function2(ip: String) -> ExitCode | ||||
| { | ||||
|  | ||||
|  | ||||
|     let mut device_online = check_connection(ip.to_string()); | ||||
|     if !device_online { | ||||
|         println!("{} not online", ip); | ||||
|        // return ExitCode::FAILURE; | ||||
|     } | ||||
|  | ||||
|      | ||||
|  | ||||
|     let receiver = openweathermap::init_forecast("Mannheim", | ||||
|     "metric", | ||||
|     "de", | ||||
|     "978882ab9dd05e7122ff2b0aef2d3e55", | ||||
|     60,1); | ||||
|  | ||||
|     let mut last_data = Option::None; | ||||
|      | ||||
|     // Test Webcrawler for public transportataion | ||||
|     let mut straba_res = straba::fetch_data(Some(true)); | ||||
|     println!("{:?} {:?}s", straba_res.outbound_station, straba_res.outbound_diff); | ||||
|     println!("{:?} {:?}s", straba_res.inbound_station , straba_res.inbound_diff); | ||||
|  | ||||
|     // Initialize MQTT client from MQTT_BROKER env var (else disabled) | ||||
|     let mqtt_client: Option<Client> = { | ||||
|         // Read broker URL from environment | ||||
|         let broker = match std::env::var("MQTT_BROKER") { | ||||
|             Ok(val) if !val.is_empty() => val, | ||||
|             _ => { | ||||
|                 eprintln!("Environment variable MQTT_BROKER not set or empty, MQTT disabled"); | ||||
|                 String::new() | ||||
|             } | ||||
|         }; | ||||
|         if broker.is_empty() { | ||||
|             None | ||||
|         } else { | ||||
|             let create_opts = CreateOptionsBuilder::new() | ||||
|                 .server_uri(&broker) | ||||
|                 .client_id("ledboard_client") | ||||
|                 .finalize(); | ||||
|             match Client::new(create_opts) { | ||||
|                 Ok(cli) => { | ||||
|                     let conn_opts = ConnectOptionsBuilder::new() | ||||
|                         .keep_alive_interval(Duration::from_secs(20)) | ||||
|                         .clean_session(true) | ||||
|                         .finalize(); | ||||
|                     match cli.connect(conn_opts) { | ||||
|                         Ok(_) => Some(cli), | ||||
|                         Err(e) => { | ||||
|                             eprintln!("Failed to connect to MQTT broker '{}': {}", broker, e); | ||||
|                             None | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     eprintln!("Failed to create MQTT client for '{}': {}", broker, e); | ||||
|                     None | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Render start | ||||
|     send_package(ip.to_string(), &last_data, &straba_res, None); | ||||
|     loop { | ||||
|         let st_now = SystemTime::now(); | ||||
|         let seconds = st_now.duration_since(UNIX_EPOCH).unwrap().as_secs(); | ||||
|         let delay = time::Duration::from_millis(500); | ||||
|         thread::sleep(delay); | ||||
|         // Only request, if the device is present | ||||
|         if device_online == true { | ||||
|             let answer = openweathermap::update_forecast(&receiver); | ||||
|             match answer { | ||||
|                 Some(_) => { | ||||
|                     last_data = answer; | ||||
|                 } | ||||
|                 None => { | ||||
|      | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (straba_res.request_time + 50) < seconds as i64 { | ||||
|             device_online = check_connection(ip.to_string()); | ||||
|             // request once a minute new data | ||||
|             if device_online == true { | ||||
|                 straba_res = straba::fetch_data(None); | ||||
|                 println!("Update {:?} {:?}s", straba_res.outbound_station, straba_res.outbound_diff); | ||||
|                 println!("Update {:?} {:?}s", straba_res.inbound_station , straba_res.inbound_diff); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if device_online == true { | ||||
|             // Render new image | ||||
|             send_package(ip.to_string(), &last_data, &straba_res, None); | ||||
|             // Publish data to MQTT | ||||
|        } | ||||
|          if let Some(ref client) = mqtt_client { | ||||
|                 publish_to_mqtt(client, &last_data, &straba_res); | ||||
|             } | ||||
|  | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn main() -> ExitCode { | ||||
| @@ -360,62 +779,16 @@ fn main() -> ExitCode { | ||||
|         // one argument passed | ||||
|         2 => { | ||||
|             let ip = &args[1]; | ||||
|  | ||||
|  | ||||
|             let mut device_online = check_connection(ip.to_string()); | ||||
|             if !device_online { | ||||
|                 println!("{} not online", ip); | ||||
|                 return ExitCode::FAILURE; | ||||
|             } | ||||
|  | ||||
|             let receiver = openweathermap::init_forecast("Mannheim", | ||||
|             "metric", | ||||
|             "de", | ||||
|             "978882ab9dd05e7122ff2b0aef2d3e55", | ||||
|             60,1); | ||||
|  | ||||
|             let mut last_data = Option::None; | ||||
|              | ||||
|             // Test Webcrawler for public transportataion | ||||
|             let mut straba_res = straba::fetch_data(Some(true)); | ||||
|             println!("{:?} {:?}s", straba_res.outbound_station, straba_res.outbound_diff); | ||||
|             println!("{:?} {:?}s", straba_res.inbound_station , straba_res.inbound_diff); | ||||
|  | ||||
|             // Render start | ||||
|             send_package(ip.to_string(), &last_data, &straba_res); | ||||
|             loop { | ||||
|                 let st_now = SystemTime::now(); | ||||
|                 let seconds = st_now.duration_since(UNIX_EPOCH).unwrap().as_secs(); | ||||
|                 let delay = time::Duration::from_millis(500); | ||||
|                 thread::sleep(delay); | ||||
|                 // Only request, if the device is present | ||||
|                 if device_online == true { | ||||
|                     let answer = openweathermap::update_forecast(&receiver); | ||||
|                     match answer { | ||||
|                         Some(_) => { | ||||
|                             last_data = answer; | ||||
|                         } | ||||
|                         None => { | ||||
|              | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (straba_res.request_time + 50) < seconds as i64 { | ||||
|                     device_online = check_connection(ip.to_string()); | ||||
|                     // request once a minute new data | ||||
|                     if device_online == true { | ||||
|                         straba_res = straba::fetch_data(None); | ||||
|                         println!("Update {:?} {:?}s", straba_res.outbound_station, straba_res.outbound_diff); | ||||
|                         println!("Update {:?} {:?}s", straba_res.inbound_station , straba_res.inbound_diff); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if device_online == true { | ||||
|                     // Render new image | ||||
|                     send_package(ip.to_string(), &last_data, &straba_res); | ||||
|                 } | ||||
|             } | ||||
|             // Only one parameter uses the logic, generated in o4mini-llm | ||||
|             return main_function2(ip.to_string()); | ||||
|         } | ||||
|         // two argument passed | ||||
|         3 => { | ||||
|             let ip = &args[1]; | ||||
|             let mqtt = &args[2]; | ||||
|             return main_function(   ip.to_string(), | ||||
|                                     Some(mqtt.to_string()) | ||||
|                                 ); | ||||
|         } | ||||
|         // all the other cases | ||||
|         _ => { | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| use chrono::DateTime; | ||||
| use std::time::{SystemTime, UNIX_EPOCH}; | ||||
|  | ||||
| use chrono::Local; | ||||
| use serde::Deserialize; | ||||
|  | ||||
| const STATION_URL:&str = "https://www.rnv-online.de/rest/departure/2494"; | ||||
| @@ -79,10 +79,11 @@ pub struct NextDeparture { | ||||
| } | ||||
|  | ||||
| pub fn fetch_data(debug_print : Option<bool>) -> NextDeparture { | ||||
|  | ||||
|     let date = Local::now(); | ||||
|     let st_now = SystemTime::now(); | ||||
|     let seconds = st_now.duration_since(UNIX_EPOCH).unwrap().as_secs(); | ||||
|     let url = &format!("{}?datetime={}", STATION_URL, seconds); | ||||
|     let timeString = date.format("%Y-%m-%d %H:%M:%S"); | ||||
|     let url = &format!("{}?datetime={}", STATION_URL, timeString); | ||||
|     let result = reqwest::blocking::get(url); | ||||
|      | ||||
|     let mut return_value = NextDeparture { | ||||
| @@ -118,11 +119,25 @@ pub fn fetch_data(debug_print : Option<bool>) -> NextDeparture { | ||||
|         return return_value; | ||||
|     } | ||||
|  | ||||
|     if debug_print.is_some() && debug_print.unwrap() == true { | ||||
|         println!("----------- Seconds {:} {:} requesting ... -----------", seconds, timeString); | ||||
|     } | ||||
|     // parse JSON result.. search of both directions | ||||
|     let json = body.unwrap(); | ||||
|      | ||||
|     if debug_print.is_some() && debug_print.unwrap() == true { | ||||
|         println!("Requesting {:}", json.graph_ql.response.name); | ||||
|         println!("Elements {:}", json.graph_ql.response.journeys.elements.len() ); | ||||
|         //println!("------------------------- %< ----------------------------"); | ||||
|         //println!("{}", &raw_text); | ||||
|         //println!("------------------------- %< ----------------------------"); | ||||
|     } | ||||
|  | ||||
|     for el in json.graph_ql.response.journeys.elements { | ||||
|  | ||||
|         if debug_print.is_some() && debug_print.unwrap() == true { | ||||
|             println!("Line {:}", el.line.line_group.label);   | ||||
|             println!("Requesting {:}", json.graph_ql.response.name); | ||||
|             println!("Line {:}", el.line.line_group.label); | ||||
|         } | ||||
|         for stop in el.stops { | ||||
|             // use only valid data | ||||
| @@ -140,6 +155,8 @@ pub fn fetch_data(debug_print : Option<bool>) -> NextDeparture { | ||||
|                     if diff <  return_value.outbound_diff { | ||||
|                         return_value.outbound_station = stop.destination_label; | ||||
|                         return_value.outbound_diff = diff; | ||||
|                     } else if debug_print.is_some() && debug_print.unwrap() == true { | ||||
|                         println!("Unkown diff Stop   {:} {:} (in {:} seconds)", stop.destination_label, txt_departure, diff ); | ||||
|                     } | ||||
|                 } else if stop.destination_label.contains("Hochschule") || | ||||
|                             stop.destination_label.contains("Hauptbahnhof") || | ||||
| @@ -147,13 +164,19 @@ pub fn fetch_data(debug_print : Option<bool>) -> NextDeparture { | ||||
|                     if diff <  return_value.inbound_diff { | ||||
|                         return_value.inbound_station = stop.destination_label; | ||||
|                         return_value.inbound_diff = diff; | ||||
|                     } else if debug_print.is_some() && debug_print.unwrap() == true { | ||||
|                         println!("Unkown diff Stop   {:} {:} (in {:} seconds)", stop.destination_label, txt_departure, diff ); | ||||
|                     } | ||||
|                 } else if debug_print.is_some() && debug_print.unwrap() == true { | ||||
|                     println!("Unkown Stop   {:} {:} (in {:} seconds)", stop.destination_label, txt_departure, diff ); | ||||
|                 } | ||||
|             } else { | ||||
|                 println!("Planned {:} {:?}", stop.destination_label, stop.planned_departure.iso_string) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if debug_print.is_some() && debug_print.unwrap() == true { | ||||
|         println!("----------- end of straba.rs -----------"); | ||||
|     } | ||||
|     return_value | ||||
| } | ||||
|   | ||||
							
								
								
									
										17
									
								
								client/ledBoard.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								client/ledBoard.service
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| [Unit] | ||||
| Description=Log uptime in scoreboard | ||||
| DefaultDependencies=no | ||||
|  | ||||
| [Service] | ||||
| Type=simple | ||||
| Restart=on-failure | ||||
| User=c3ma | ||||
|  | ||||
| # Specify users home as working directory | ||||
| WorkingDirectory=/home/c3ma/ | ||||
| # Define wrapper to update and start project | ||||
| ExecStart=/usr/bin/bash <project home>/client/ledboard.sh | ||||
| TimeoutStartSec=0 | ||||
|  | ||||
| [Install] | ||||
| WantedBy=network.target | ||||
							
								
								
									
										12
									
								
								client/ledboard.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										12
									
								
								client/ledboard.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| #!/bin/bash | ||||
| # Wrapper script to update project and build project | ||||
| # | ||||
| #Set target IP address | ||||
| IP= | ||||
| # Path to this project | ||||
| HOSTCLIENT= | ||||
| cd $HOSTCLIENT | ||||
| /usr/bin/pkill LEDboardClient | ||||
| git pull | ||||
| cargo build | ||||
| $HOSTCLIENT/target/debug/LEDboardClient $IP | ||||
		Reference in New Issue
	
	Block a user