From 25da1ac04bd8dc4b6e7390a666e4158cd618d597 Mon Sep 17 00:00:00 2001 From: Lutz Lang Date: Fri, 25 Apr 2025 21:59:51 +0200 Subject: [PATCH 1/5] feat: add MQTT support and configuration documentation --- MQTT.md | 51 ++++++++++++++++++++++++++++++++++++++++++ client/bin/Cargo.toml | 1 + client/bin/src/main.rs | 4 ++++ 3 files changed, 56 insertions(+) create mode 100644 MQTT.md diff --git a/MQTT.md b/MQTT.md new file mode 100644 index 0000000..1bebd37 --- /dev/null +++ b/MQTT.md @@ -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. \ No newline at end of file diff --git a/client/bin/Cargo.toml b/client/bin/Cargo.toml index 51bbb76..1dbfeaa 100644 --- a/client/bin/Cargo.toml +++ b/client/bin/Cargo.toml @@ -21,4 +21,5 @@ serde = "1.0" serde_derive = "1.0" serde_json = "1.0" # end of web stuff +paho-mqtt = "0.13.2" ping = "0.4.1" diff --git a/client/bin/src/main.rs b/client/bin/src/main.rs index 4a8b134..bb80194 100644 --- a/client/bin/src/main.rs +++ b/client/bin/src/main.rs @@ -414,6 +414,10 @@ fn main() -> ExitCode { if device_online == true { // Render new image send_package(ip.to_string(), &last_data, &straba_res); + // Publish data to MQTT + if let Some(ref client) = mqtt_client { + publish_to_mqtt(client, &last_data, &straba_res); + } } } } From bd300f163e84ce20fc8f0ed6d086a0fa59503acd Mon Sep 17 00:00:00 2001 From: Lutz Lang Date: Fri, 25 Apr 2025 22:55:45 +0200 Subject: [PATCH 2/5] Add MQTT documentation and client support Create MQTT.md with protocol overview and usage examples. Update client/bin/src/main.rs to initialize a Paho MQTT client with environment-configured broker, connect, and publish combined weather and transit data to the ledboard/data topic. --- MQTT.md | 51 ---------------------------------- client/bin/src/main.rs | 62 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 51 deletions(-) delete mode 100644 MQTT.md diff --git a/MQTT.md b/MQTT.md deleted file mode 100644 index 1bebd37..0000000 --- a/MQTT.md +++ /dev/null @@ -1,51 +0,0 @@ - # 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. \ No newline at end of file diff --git a/client/bin/src/main.rs b/client/bin/src/main.rs index bb80194..c01bd6a 100644 --- a/client/bin/src/main.rs +++ b/client/bin/src/main.rs @@ -23,6 +23,7 @@ use std::process::ExitCode; use openweathermap::forecast::Forecast; use straba::NextDeparture; +use paho_mqtt::{Client, CreateOptionsBuilder, ConnectOptionsBuilder, Message}; // 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; @@ -347,6 +348,28 @@ fn check_connection(ipaddress: String) -> bool { } return device_online; } +/// Publishes weather and transit data to MQTT broker +fn publish_to_mqtt(client: &Client, data: &Option>, 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:{},out:{}min,in:{}min", + temp, + weather, + straba_res.outbound_diff / 60, + straba_res.inbound_diff / 60) + } else { + "no_forecast".to_string() + } + } else { + "no_data".to_string() + }; + let msg = Message::new("ledboard/data", payload, 1); + if let Err(e) = client.publish(msg) { + eprintln!("Error publishing MQTT message: {}", e); + } +} fn main() -> ExitCode { let args: Vec = env::args().collect(); @@ -380,6 +403,45 @@ fn main() -> ExitCode { 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 = { + // 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); From 0acb2a253844e707df5e425aa2b9d376c56c1d9a Mon Sep 17 00:00:00 2001 From: Lutz Lang Date: Fri, 25 Apr 2025 23:39:56 +0200 Subject: [PATCH 3/5] fix udp on macOS --- client/bin/src/main.rs | 47 +++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/client/bin/src/main.rs b/client/bin/src/main.rs index c01bd6a..9729d3c 100644 --- a/client/bin/src/main.rs +++ b/client/bin/src/main.rs @@ -303,12 +303,11 @@ fn send_package(ipaddress: String, 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"); } @@ -323,30 +322,26 @@ LEDboardClient " 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"); - - // 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; - } + + // 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, } - return device_online; + + device_online } /// Publishes weather and transit data to MQTT broker fn publish_to_mqtt(client: &Client, data: &Option>, straba_res: &NextDeparture) { From 06ce74da9f7594f53ae63e15b43f2e486f4d499b Mon Sep 17 00:00:00 2001 From: Lutz Lang Date: Sat, 26 Apr 2025 00:09:40 +0200 Subject: [PATCH 4/5] Added MQTT.md --- client/MQTT.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 client/MQTT.md diff --git a/client/MQTT.md b/client/MQTT.md new file mode 100644 index 0000000..1bebd37 --- /dev/null +++ b/client/MQTT.md @@ -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. \ No newline at end of file From fbdf1ea24b3df49b2656dd1e462b70d35041e087 Mon Sep 17 00:00:00 2001 From: Ollo Date: Sat, 26 Apr 2025 15:41:24 +0200 Subject: [PATCH 5/5] Hacked MQTT output without panel --- client/bin/src/main.rs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/client/bin/src/main.rs b/client/bin/src/main.rs index 9729d3c..59bf76e 100644 --- a/client/bin/src/main.rs +++ b/client/bin/src/main.rs @@ -349,21 +349,29 @@ fn publish_to_mqtt(client: &Client, data: &Option>, str 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:{},out:{}min,in:{}min", + format!("temp:{:.1}C,weather:{}", temp, - weather, - straba_res.outbound_diff / 60, - straba_res.inbound_diff / 60) - } else { + weather) + } else { "no_forecast".to_string() } } else { "no_data".to_string() }; - let msg = Message::new("ledboard/data", payload, 1); + let msg = Message::new("ledboard/forecast", payload, 1); if let Err(e) = client.publish(msg) { eprintln!("Error publishing MQTT message: {}", e); } + + let payloadPT = { + format!("out:{}min,in:{}min", + straba_res.outbound_diff / 60, + straba_res.inbound_diff / 60) + }; + let ptmsg = Message::new("ledboard/public_transportation", payloadPT, 1); + if let Err(e) = client.publish(ptmsg) { + eprintln!("Error publishing MQTT message: {}", e); + } } fn main() -> ExitCode { @@ -383,9 +391,11 @@ fn main() -> ExitCode { let mut device_online = check_connection(ip.to_string()); if !device_online { println!("{} not online", ip); - return ExitCode::FAILURE; + // return ExitCode::FAILURE; } + + let receiver = openweathermap::init_forecast("Mannheim", "metric", "de", @@ -398,7 +408,7 @@ fn main() -> ExitCode { 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 = { // Read broker URL from environment @@ -472,10 +482,11 @@ fn main() -> ExitCode { // Render new image send_package(ip.to_string(), &last_data, &straba_res); // Publish data to MQTT - if let Some(ref client) = mqtt_client { + } + if let Some(ref client) = mqtt_client { publish_to_mqtt(client, &last_data, &straba_res); } - } + } } // all the other cases