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/launch.json
|
||||||
.vscode/ipch
|
.vscode/ipch
|
||||||
todo.txt
|
todo.txt
|
||||||
|
*.ini
|
||||||
|
14
README.md
14
README.md
@ -25,3 +25,17 @@ stored in folder **/client**
|
|||||||
go to **/client**
|
go to **/client**
|
||||||
* cargo build
|
* cargo build
|
||||||
* cargo run
|
* 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"
|
serde_json = "1.0"
|
||||||
# end of web stuff
|
# end of web stuff
|
||||||
ping = "0.4.1"
|
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::{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 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 std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use openweathermap::forecast::Weather;
|
use openweathermap::{forecast::Weather, Receiver};
|
||||||
use substring::Substring;
|
use substring::Substring;
|
||||||
use tinybmp::Bmp;
|
use tinybmp::Bmp;
|
||||||
use core::time;
|
use core::time;
|
||||||
@ -18,15 +21,23 @@ use embedded_graphics::{
|
|||||||
|
|
||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
use std::{env, thread};
|
use std::{env, thread};
|
||||||
|
use std::path::Path;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use openweathermap::forecast::Forecast;
|
use openweathermap::forecast::Forecast;
|
||||||
use straba::NextDeparture;
|
use straba::NextDeparture;
|
||||||
// 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;
|
||||||
|
|
||||||
|
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_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: u32 = 5 * 32;
|
||||||
const IMAGE_WIDTH_BYTE: u32 = IMAGE_WIDTH / 8; /* one byte contains 8 LEDs, one in each bit */
|
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 IMAGE_LENGTH: usize = (IMAGE_WIDTH_BYTE * IMAGE_HEIGHT_BYTE) as usize;
|
||||||
const PACKAGE_LENGTH: usize = (IMAGE_LENGTH + 1) 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 {
|
struct UdpDisplay {
|
||||||
image: [u8; IMAGE_SIZE_BYTE],
|
image: [u8; IMAGE_SIZE_BYTE],
|
||||||
@ -243,6 +285,13 @@ fn render_clock(display: &mut UdpDisplay){
|
|||||||
.unwrap();
|
.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) {
|
fn render_strab_partial(display: &mut UdpDisplay, station: &String, diff: i64, height: i32) {
|
||||||
let text_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
|
let text_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
|
||||||
let mut diff_str = format!("{}min", (diff / 60));
|
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) {
|
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.outbound_station, straba_res.outbound_diff, 17);
|
||||||
render_strab_partial(display, &straba_res.inbound_station, straba_res.inbound_diff, 25);
|
render_strab_partial(display, &straba_res.inbound_station, straba_res.inbound_diff, 27);
|
||||||
}
|
}
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
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,
|
||||||
|
mqtt_message: Option<String>) {
|
||||||
let mut package: [u8; PACKAGE_LENGTH] = [0; PACKAGE_LENGTH];
|
let mut package: [u8; PACKAGE_LENGTH] = [0; PACKAGE_LENGTH];
|
||||||
|
|
||||||
// Brightness
|
// Brightness
|
||||||
@ -300,14 +351,17 @@ fn send_package(ipaddress: String,
|
|||||||
render_strab(&mut display, straba_res);
|
render_strab(&mut display, straba_res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mqtt_message.is_some() {
|
||||||
|
render_mqtt_message(&mut display, mqtt_message.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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -318,55 +372,214 @@ LEDboardClient <ip address>"
|
|||||||
);
|
);
|
||||||
println!("one argument necessary!");
|
println!("one argument necessary!");
|
||||||
println!("<ip address>");
|
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 {
|
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");
|
|
||||||
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]
|
// Use a random local port instead of hardcoding
|
||||||
let answer = socket.recv_from(&mut package);
|
let socket = UdpSocket::bind("0.0.0.0:0").expect("couldn't bind to address");
|
||||||
match answer {
|
socket.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
|
||||||
Ok((_n, _addr)) => {
|
|
||||||
//println!("{} bytes response from {:?} {:?}", n, addr, &package[..n]);
|
let target = format!("{}:4242", ipaddress);
|
||||||
device_online = true;
|
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(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
},
|
||||||
device_online = false;
|
Err(_) => device_online = false,
|
||||||
}
|
|
||||||
Err(_e) => {
|
|
||||||
device_online = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return device_online;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
device_online
|
||||||
let args: Vec<String> = env::args().collect();
|
}
|
||||||
match args.len() {
|
/// Publishes weather and transit data to MQTT broker
|
||||||
// no arguments passed
|
fn publish_to_mqtt(client: &Client, data: &Option<Result<Forecast, String>>, straba_res: &NextDeparture) {
|
||||||
1 => {
|
let payload = if let Some(Ok(forecast)) = data {
|
||||||
// show a help message
|
if let Some(f) = forecast.list.first() {
|
||||||
help();
|
let temp = f.main.temp;
|
||||||
return ExitCode::SUCCESS;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// one argument passed
|
|
||||||
2 => {
|
|
||||||
let ip = &args[1];
|
|
||||||
|
|
||||||
|
|
||||||
let mut device_online = check_connection(ip.to_string());
|
|
||||||
if !device_online {
|
/// Asynchronously publishes a message to the specified topic.
|
||||||
println!("{} not online", ip);
|
///
|
||||||
|
/// @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:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
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",
|
let receiver = openweathermap::init_forecast("Mannheim",
|
||||||
"metric",
|
"metric",
|
||||||
@ -381,8 +594,139 @@ fn main() -> ExitCode {
|
|||||||
println!("{:?} {:?}s", straba_res.outbound_station, straba_res.outbound_diff);
|
println!("{:?} {:?}s", straba_res.outbound_station, straba_res.outbound_diff);
|
||||||
println!("{:?} {:?}s", straba_res.inbound_station , straba_res.inbound_diff);
|
println!("{:?} {:?}s", straba_res.inbound_station , straba_res.inbound_diff);
|
||||||
|
|
||||||
|
if GlobalConfiguration.lock().is_ok() && (GlobalConfiguration.lock().unwrap().panelIPAddress.len() > 0)
|
||||||
|
{
|
||||||
// Render start
|
// Render start
|
||||||
send_package(ip.to_string(), &last_data, &straba_res);
|
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 {
|
loop {
|
||||||
let st_now = SystemTime::now();
|
let st_now = SystemTime::now();
|
||||||
let seconds = st_now.duration_since(UNIX_EPOCH).unwrap().as_secs();
|
let seconds = st_now.duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||||
@ -413,9 +757,38 @@ fn main() -> ExitCode {
|
|||||||
|
|
||||||
if device_online == true {
|
if device_online == true {
|
||||||
// Render new image
|
// Render new image
|
||||||
send_package(ip.to_string(), &last_data, &straba_res);
|
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 {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
match args.len() {
|
||||||
|
// no arguments passed
|
||||||
|
1 => {
|
||||||
|
// show a help message
|
||||||
|
help();
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
// one argument passed
|
||||||
|
2 => {
|
||||||
|
let ip = &args[1];
|
||||||
|
// 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
|
// all the other cases
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use chrono::Local;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
const STATION_URL:&str = "https://www.rnv-online.de/rest/departure/2494";
|
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 {
|
pub fn fetch_data(debug_print : Option<bool>) -> NextDeparture {
|
||||||
|
let date = Local::now();
|
||||||
let st_now = SystemTime::now();
|
let st_now = SystemTime::now();
|
||||||
let seconds = st_now.duration_since(UNIX_EPOCH).unwrap().as_secs();
|
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 result = reqwest::blocking::get(url);
|
||||||
|
|
||||||
let mut return_value = NextDeparture {
|
let mut return_value = NextDeparture {
|
||||||
@ -118,10 +119,24 @@ pub fn fetch_data(debug_print : Option<bool>) -> NextDeparture {
|
|||||||
return return_value;
|
return return_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if debug_print.is_some() && debug_print.unwrap() == true {
|
||||||
|
println!("----------- Seconds {:} {:} requesting ... -----------", seconds, timeString);
|
||||||
|
}
|
||||||
// parse JSON result.. search of both directions
|
// parse JSON result.. search of both directions
|
||||||
let json = body.unwrap();
|
let json = body.unwrap();
|
||||||
for el in json.graph_ql.response.journeys.elements {
|
|
||||||
if debug_print.is_some() && debug_print.unwrap() == true {
|
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!("Requesting {:}", json.graph_ql.response.name);
|
||||||
println!("Line {:}", el.line.line_group.label);
|
println!("Line {:}", el.line.line_group.label);
|
||||||
}
|
}
|
||||||
for stop in el.stops {
|
for stop in el.stops {
|
||||||
@ -140,6 +155,8 @@ pub fn fetch_data(debug_print : Option<bool>) -> NextDeparture {
|
|||||||
if diff < return_value.outbound_diff {
|
if diff < return_value.outbound_diff {
|
||||||
return_value.outbound_station = stop.destination_label;
|
return_value.outbound_station = stop.destination_label;
|
||||||
return_value.outbound_diff = diff;
|
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") ||
|
} else if stop.destination_label.contains("Hochschule") ||
|
||||||
stop.destination_label.contains("Hauptbahnhof") ||
|
stop.destination_label.contains("Hauptbahnhof") ||
|
||||||
@ -147,13 +164,19 @@ pub fn fetch_data(debug_print : Option<bool>) -> NextDeparture {
|
|||||||
if diff < return_value.inbound_diff {
|
if diff < return_value.inbound_diff {
|
||||||
return_value.inbound_station = stop.destination_label;
|
return_value.inbound_station = stop.destination_label;
|
||||||
return_value.inbound_diff = diff;
|
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 {
|
} else {
|
||||||
println!("Planned {:} {:?}", stop.destination_label, stop.planned_departure.iso_string)
|
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
|
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
|
Loading…
x
Reference in New Issue
Block a user