Compare commits
16 Commits
122b6e58b3
...
o4mini-llm
Author | SHA1 | Date | |
---|---|---|---|
|
a0e18c1a7d | ||
|
fbdf1ea24b | ||
06ce74da9f | |||
0acb2a2538 | |||
bd300f163e | |||
25da1ac04b | |||
|
47f5507f4f | ||
|
6744004387 | ||
|
e42b08bcf3 | ||
|
5923bc830e | ||
|
b963abc80b | ||
|
e07413f594 | ||
|
870cd0ad0a | ||
|
04aab61ab7 | ||
|
46f7eb4f8a | ||
|
860815313b |
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.
|
@@ -21,4 +21,5 @@ serde = "1.0"
|
|||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
# end of web stuff
|
# end of web stuff
|
||||||
|
paho-mqtt = "0.13.2"
|
||||||
ping = "0.4.1"
|
ping = "0.4.1"
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
use std::{time::Duration, fmt::format};
|
use std::{time::Duration, fmt::format};
|
||||||
|
use str;
|
||||||
use bit::BitIndex;
|
use bit::BitIndex;
|
||||||
use chrono_tz::Europe::Berlin;
|
use chrono_tz::Europe::Berlin;
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc, Timelike};
|
use chrono::{DateTime, NaiveDateTime, Utc, Timelike};
|
||||||
use chrono::prelude::*;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use openweathermap::forecast::Weather;
|
use openweathermap::forecast::Weather;
|
||||||
use substring::Substring;
|
use substring::Substring;
|
||||||
@@ -10,7 +10,7 @@ use tinybmp::Bmp;
|
|||||||
use core::time;
|
use core::time;
|
||||||
use embedded_graphics::{
|
use embedded_graphics::{
|
||||||
image::Image,
|
image::Image,
|
||||||
mono_font::{iso_8859_1::FONT_6X10, iso_8859_1::FONT_5X8, MonoTextStyle},
|
mono_font::{iso_8859_1::FONT_6X10, iso_8859_1::FONT_5X8, iso_8859_1::FONT_4X6, MonoTextStyle},
|
||||||
pixelcolor::BinaryColor,
|
pixelcolor::BinaryColor,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
text::Text,
|
text::Text,
|
||||||
@@ -19,9 +19,11 @@ use embedded_graphics::{
|
|||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
use std::{env, thread};
|
use std::{env, thread};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use openweathermap::forecast::Forecast;
|
use openweathermap::forecast::Forecast;
|
||||||
use straba::NextDeparture;
|
use straba::NextDeparture;
|
||||||
|
use paho_mqtt::{Client, CreateOptionsBuilder, ConnectOptionsBuilder, Message};
|
||||||
// This declaration will look for a file named `straba.rs` and will
|
// This declaration will look for a file named `straba.rs` and will
|
||||||
// insert its contents inside a module named `straba` under this scope
|
// insert its contents inside a module named `straba` under this scope
|
||||||
mod straba;
|
mod straba;
|
||||||
@@ -242,6 +244,43 @@ fn render_clock(display: &mut UdpDisplay){
|
|||||||
.unwrap();
|
.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));
|
||||||
|
if diff < 60 {
|
||||||
|
diff_str = String::from("sofort");
|
||||||
|
}
|
||||||
|
let station_short: String;
|
||||||
|
if str::len(&station) > 13 {
|
||||||
|
station_short = station
|
||||||
|
.replace("Straße", "Str")
|
||||||
|
.replace("straße", "str")
|
||||||
|
.replace("Platz", "Pl")
|
||||||
|
.replace("platz", "pl")
|
||||||
|
.replace("Hauptbahnhof", "Hbf")
|
||||||
|
.replace("Bahnhof", "Bf");
|
||||||
|
} else {
|
||||||
|
station_short = station.to_string();
|
||||||
|
}
|
||||||
|
let station_clip: String;
|
||||||
|
if str::len(&station_short) > 13 {
|
||||||
|
station_clip = station_short[0..12].to_string();
|
||||||
|
} else {
|
||||||
|
station_clip = station_short.to_string();
|
||||||
|
}
|
||||||
|
Text::new(&station_clip, Point::new(1, height), text_style)
|
||||||
|
.draw(display)
|
||||||
|
.unwrap();
|
||||||
|
Text::new(&diff_str, Point::new(80, height), text_style)
|
||||||
|
.draw(display)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
fn send_package(ipaddress: String,
|
fn send_package(ipaddress: String,
|
||||||
data: &Option<Result<Forecast, String>>,
|
data: &Option<Result<Forecast, String>>,
|
||||||
straba_res: &NextDeparture) {
|
straba_res: &NextDeparture) {
|
||||||
@@ -259,43 +298,16 @@ fn send_package(ipaddress: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if straba_res.failure == false {
|
if straba_res.failure == false {
|
||||||
let text_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
|
render_strab(&mut display, straba_res);
|
||||||
let text_style_station = MonoTextStyle::new(&FONT_5X8, BinaryColor::On);
|
|
||||||
let mut outbound = format!("{}min", (straba_res.outbound_diff / 60));
|
|
||||||
if straba_res.outbound_diff < 60 {
|
|
||||||
outbound = String::from("sofort");
|
|
||||||
}
|
|
||||||
Text::new(&straba_res.outbound_station, Point::new(1, 15), text_style_station)
|
|
||||||
.draw(&mut display)
|
|
||||||
.unwrap();
|
|
||||||
Text::new(&outbound, Point::new(80, 15), text_style)
|
|
||||||
.draw(&mut display)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut inbound = format!("{}min", (straba_res.inbound_diff / 60));
|
|
||||||
if straba_res.inbound_diff < 60 {
|
|
||||||
inbound = String::from("sofort");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Text::new(&straba_res.inbound_station, Point::new(1, 25), text_style_station)
|
|
||||||
.draw(&mut display)
|
|
||||||
.unwrap();
|
|
||||||
Text::new(&inbound, Point::new(80, 24), text_style)
|
|
||||||
.draw(&mut display)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
render_clock(&mut display);
|
render_clock(&mut display);
|
||||||
|
|
||||||
|
|
||||||
package[1..PACKAGE_LENGTH].copy_from_slice(&display.image);
|
package[1..PACKAGE_LENGTH].copy_from_slice(&display.image);
|
||||||
// client need to bind to client port (1 before 4242)
|
let target = format!("{}:4242", ipaddress);
|
||||||
let socket = UdpSocket::bind("0.0.0.0:14242").expect("couldn't bind to address");
|
let socket = UdpSocket::bind("0.0.0.0:0").expect("couldn't bind to address");
|
||||||
socket
|
socket
|
||||||
.send_to(&package, ipaddress + ":4242")
|
.send_to(&package, &target)
|
||||||
.expect("couldn't send data");
|
.expect("couldn't send data");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,105 +322,193 @@ LEDboardClient <ip address>"
|
|||||||
|
|
||||||
fn check_connection(ipaddress: String) -> bool {
|
fn check_connection(ipaddress: String) -> bool {
|
||||||
let device_online;
|
let device_online;
|
||||||
// generate a faulty package length
|
|
||||||
let mut package: [u8; PACKAGE_LENGTH/2] = [0; PACKAGE_LENGTH/2];
|
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");
|
// Use a random local port instead of hardcoding
|
||||||
socket.set_read_timeout(Some(Duration::from_secs(10))).unwrap(); /* 10 seconds timeout */
|
let socket = UdpSocket::bind("0.0.0.0:0").expect("couldn't bind to address");
|
||||||
socket
|
socket.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
|
||||||
.send_to(&package, ipaddress + ":4242")
|
|
||||||
.expect("couldn't send data");
|
let target = format!("{}:4242", ipaddress);
|
||||||
|
match socket.send_to(&package, &target) {
|
||||||
// self.recv_buff is a [u8; 8092]
|
Ok(_) => {
|
||||||
let answer = socket.recv_from(&mut package);
|
// Continue with receive
|
||||||
match answer {
|
match socket.recv_from(&mut package) {
|
||||||
Ok((_n, _addr)) => {
|
Ok((_n, _addr)) => device_online = true,
|
||||||
//println!("{} bytes response from {:?} {:?}", n, addr, &package[..n]);
|
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => device_online = false,
|
||||||
device_online = true;
|
Err(_) => device_online = false,
|
||||||
}
|
}
|
||||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
},
|
||||||
device_online = false;
|
Err(_) => device_online = false,
|
||||||
}
|
}
|
||||||
Err(_e) => {
|
|
||||||
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 = 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);
|
||||||
}
|
}
|
||||||
return device_online;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() -> ExitCode {
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
match args.len() {
|
match args.len() {
|
||||||
// no arguments passed
|
// no arguments passed
|
||||||
1 => {
|
1 => {
|
||||||
// show a help message
|
// show a help message
|
||||||
help();
|
help();
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
}
|
}
|
||||||
// one argument passed
|
// one argument passed
|
||||||
2 => {
|
2 => {
|
||||||
let ip = &args[1];
|
let ip = &args[1];
|
||||||
|
|
||||||
|
// Read broker URL from environment
|
||||||
let mut device_online = check_connection(ip.to_string());
|
let broker = match std::env::var("MQTT_BROKER") {
|
||||||
if !device_online {
|
Ok(val) if !val.is_empty() => val,
|
||||||
println!("{} not online", ip);
|
_ => {
|
||||||
return
|
eprintln!("Environment variable MQTT_BROKER not set or empty, MQTT disabled");
|
||||||
}
|
String::new()
|
||||||
|
|
||||||
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 + 60) < seconds as i64 {
|
return main_function(ip, Some(broker))
|
||||||
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
|
// two argument passed
|
||||||
send_package(ip.to_string(), &last_data, &straba_res);
|
3 => {
|
||||||
}
|
let ip = &args[1];
|
||||||
}
|
let mqtt = &args[2];
|
||||||
|
return main_function( ip,
|
||||||
|
Some(mqtt.to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// all the other cases
|
// all the other cases
|
||||||
_ => {
|
_ => {
|
||||||
// show a help message
|
// show a help message
|
||||||
help();
|
help();
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn main_function(ip: &String, mqttBroker: Option<String>) -> ExitCode {
|
||||||
|
let mut device_online: bool = 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> = {
|
||||||
|
if mqttBroker.is_none() {
|
||||||
|
None
|
||||||
|
} else if mqttBroker.is_some() {
|
||||||
|
let create_opts = CreateOptionsBuilder::new()
|
||||||
|
.server_uri(mqttBroker.clone().unwrap())
|
||||||
|
.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 '{}': {}", mqttBroker.clone().unwrap(), e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create MQTT client for '{}': {}", mqttBroker.clone().unwrap(), e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
// Publish data to MQTT
|
||||||
|
}
|
||||||
|
if let Some(ref client) = mqtt_client {
|
||||||
|
publish_to_mqtt(client, &last_data, &straba_res);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -22,6 +22,7 @@ MainWindow::~MainWindow()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::drawImage(QImage *target) {
|
void MainWindow::drawImage(QImage *target) {
|
||||||
|
(void)target; /* handle unused variable ;-) */
|
||||||
this->mScene=new QGraphicsScene() ;
|
this->mScene=new QGraphicsScene() ;
|
||||||
QGraphicsView *graphicsView = new QGraphicsView();
|
QGraphicsView *graphicsView = new QGraphicsView();
|
||||||
graphicsView->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
|
graphicsView->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
|
||||||
|
Reference in New Issue
Block a user