diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..04910e0 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,36 @@ +use config::{Config, FileFormat}; +use std::fs::File; +use std::io::Write; + +use crate::ui_code::AppState; + +pub fn load_config() -> AppState { + let settings = Config::builder() + .add_source(config::File::new("./settings.toml", FileFormat::Toml)) + .build(); + + if let Ok(val) = settings { + if let Ok(state) = val.try_deserialize() { + state + } else { + AppState::default() + } + } else { + AppState::default() + } +} + +pub fn save_config(config: &AppState) { + println!("{}", { + if let Ok(toml_str) = toml::to_string(&config) { + if let Ok(mut file) = File::create("./settings.toml") { + file.write_all(toml_str.as_bytes()).unwrap(); + "" + } else { + "File could not be opened" + } + } else { + "Settings could not be deserialized" + } + }); +} diff --git a/src/joystick_loop.rs b/src/joystick_loop.rs new file mode 100644 index 0000000..433b677 --- /dev/null +++ b/src/joystick_loop.rs @@ -0,0 +1,162 @@ +use crate::config::save_config; +use crate::ui_code::{AppState, SocketConnectionUpdate}; +use crate::JoystickThreadUpdate; + +use async_channel::{Receiver, Sender}; +use gilrs::{ev::filter::FilterFn, Axis, Button, Event, EventType, Filter, Gilrs, GilrsBuilder}; +use std::panic::{self, AssertUnwindSafe}; +use std::{ + sync::{atomic::AtomicBool, Arc}, + time::{Duration, Instant}, +}; +use websocket::client::{sync::Client, ClientBuilder}; +use websocket::Message; + +struct UnknownSlayer; + +impl FilterFn for UnknownSlayer { + fn filter(&self, ev: Option, _gilrs: &mut Gilrs) -> Option { + match ev { + Some(Event { + event: EventType::ButtonPressed(Button::Unknown, ..), + id, + .. + }) + | Some(Event { + event: EventType::ButtonReleased(Button::Unknown, ..), + id, + .. + }) + | Some(Event { + event: EventType::AxisChanged(Axis::Unknown, ..), + id, + .. + }) => Some(Event::new(id, EventType::Dropped)), + _ => ev, + } + } +} + +pub fn joystick_websocket_loop( + tx: Sender, + do_run: Arc, + rx: Receiver, +) { + let mut gilrs = GilrsBuilder::new().set_update_state(false).build().unwrap(); + + let mut ip: String; + let mut port: u32; + + let mut websocket: Option> = None; + + let mut curr_x: i32 = 0; + let mut curr_y: i32 = 0; + + let mut last_update_time = Instant::now(); + + loop { + match rx.try_recv() { + Ok(msg) => { + ip = msg.ip; + port = msg.port; + + save_config(&AppState { + ip: ip.clone(), + port, + }); + + println!("ws://{}:{}", ip, port); + + if let Some(mut x) = websocket { + println!("closing websocket"); + x.send_message(&Message::close()).unwrap(); + x.shutdown().unwrap(); + websocket = None; + } else { + websocket = { + if let Ok(mut val) = + ClientBuilder::new(format!("ws://{}:{}", ip, port).as_str()) + { + if let Ok(val2) = val.connect_insecure() { + Some(val2) + } else { + println!("couldn't connect websocket! : Step 1"); + None + } + } else { + println!("couldn't connect websocket! : Step 2"); + None + } + }; + } + } + Err(async_channel::TryRecvError::Closed) => break, + Err(async_channel::TryRecvError::Empty) => {} + } + + match panic::catch_unwind(AssertUnwindSafe(|| { + while let Some(evt) = gilrs.next_event().filter_ev(&UnknownSlayer {}, &mut gilrs) { + match evt.event { + gilrs::EventType::AxisChanged(gilrs::Axis::LeftStickY, val, _) => { + curr_y = (val * 100.0) as i32; + if curr_y > -10 && curr_y < 10 { + curr_y = 0; + } + } + gilrs::EventType::AxisChanged(gilrs::Axis::LeftStickX, val, _) => { + curr_x = (val * 100.0) as i32; + if curr_x > -10 && curr_x < 10 { + curr_x = 0; + } + } + _ => {} + } + } + })) { + Ok(_) => {} + Err(_) => { + println!("panic-causing event captured") + } + } + + if websocket.is_some() + && Instant::now().duration_since(last_update_time) >= Duration::from_millis(150) + { + let mut message: String; + if curr_y > 0 { + message = format!("D{}:", curr_y); + } else { + message = format!("U{}:", curr_y.abs()); + } + + if curr_x > 0 { + message.push_str(&format!("R{}", curr_x)); + } else { + message.push_str(&format!("L{}", curr_x.abs())); + } + + if let Some(mut websocket_tx) = websocket { + websocket_tx.send_message(&Message::text(message)).unwrap(); + websocket = Some(websocket_tx); + } + last_update_time = Instant::now(); + continue; + } + + match tx.try_send(JoystickThreadUpdate { + connected: websocket.is_some(), + x_axis: Some(curr_x.to_string()), + y_axis: Some(curr_y.to_string()), + }) { + Ok(_) => {} + Err(async_channel::TrySendError::Closed(_)) => break, + Err(async_channel::TrySendError::Full(_)) => {} + } + + if !do_run.load(std::sync::atomic::Ordering::SeqCst) { + println!("Exiting thread"); + break; + } + std::thread::sleep(Duration::from_millis(25)); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1397908 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,44 @@ +use gtk::gdk::Display; +use gtk::{glib, Application}; +use gtk::{prelude::*, CssProvider}; +use std::env; + +mod config; +mod joystick_loop; +mod ui_code; +const APP_ID: &str = "net.nickiel.joystick-controller-client"; + +pub struct JoystickThreadUpdate { + pub connected: bool, + pub x_axis: Option, + pub y_axis: Option, +} + +fn main() -> glib::ExitCode { + env::set_var("gtk_csd", "0"); + + // Create a new application + let app = Application::builder().application_id(APP_ID).build(); + + // Connect to "activate" signal of `app` + app.connect_startup(|_| load_css()); + app.connect_activate(ui_code::build_ui); + + // Run the application + let exit_code = app.run(); + + println!("Closing down"); + + exit_code +} + +fn load_css() { + let provider = CssProvider::new(); + provider.load_from_string(include_str!("../style.css")); + + gtk::style_context_add_provider_for_display( + &Display::default().expect("Could not connect to a display"), + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ) +} diff --git a/src/ui_code.rs b/src/ui_code.rs new file mode 100644 index 0000000..ed040ec --- /dev/null +++ b/src/ui_code.rs @@ -0,0 +1,147 @@ +use gtk::{glib, prelude::*, Box, Entry, Label, ListBox}; +use gtk::{Application, ApplicationWindow, Button}; +use serde::{Deserialize, Serialize}; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +use crate::config::load_config; +use crate::{joystick_loop, JoystickThreadUpdate}; + +pub struct SocketConnectionUpdate { + pub ip: String, + pub port: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AppState { + pub ip: String, + pub port: u32, +} + +impl Default for AppState { + fn default() -> Self { + AppState { + ip: "10.0.0.29".to_string(), + port: 8765, + } + } +} + +pub fn build_ui(app: &Application) { + let initial_settings = load_config(); + let main_box = ListBox::new(); + + let do_run: Arc = Arc::new(AtomicBool::new(true)); + let do_run2 = do_run.clone(); + let (tx, rx) = async_channel::bounded::(4); + + let (tx2, rx2) = async_channel::bounded::(1); + + let _ = std::thread::spawn(move || joystick_loop::joystick_websocket_loop(tx, do_run2, rx2)); + + // let conn_status_label = Label::new(Some(&"No Connection".to_string())); + let conn_status_label = Label::builder() + .label("No Connection".to_string()) + .can_focus(true) + .build(); + + let content_box = Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(10) + .margin_top(12) + .margin_start(24) + .margin_end(24) + .margin_bottom(12) + .build(); + + let ip_entry = Entry::builder() + .placeholder_text("IP Address") + .text(initial_settings.ip) + .can_focus(true) + .build(); + let port_entry = Entry::builder() + .placeholder_text("Port") + .text(initial_settings.port.to_string()) + .can_focus(true) + .build(); + let button = Button::builder().margin_top(12).build(); + + content_box.append(&ip_entry); + content_box.append(&port_entry); + content_box.append(&button); + + let axis_label = Label::builder() + .label("X: 0 Y: )") + .justify(gtk::Justification::Center) + .css_classes(vec!["JoystickCurrent"]) + .build(); + + main_box.append(&conn_status_label); + main_box.append(&content_box); + main_box.append(&axis_label); + + // Connect to "clicked" signal of `button` + button.connect_clicked(glib::clone!(@weak ip_entry, @weak port_entry, @strong tx2 => move |_button| { + // Set the label to "Hello World!" after the button has been clicked on + + let ip_text = ip_entry.text(); + let port_text = port_entry.text(); + + if ip_text.len() > 0 { + if let Ok(val) = port_text.parse::() { + match tx2.try_send(SocketConnectionUpdate { + ip: ip_text.to_string(), + port: val, + }) { + Ok(_) => { } + Err(async_channel::TrySendError::Closed(_)) => {panic!("Joystick thread was closed. Unrecoverable")} + Err(e) => {println!("There was an error: {e}")} + } + } + } + })); + + glib::spawn_future_local( + glib::clone!(@weak axis_label, @weak button, @weak conn_status_label, @weak ip_entry, @weak port_entry, @strong rx => async move { + println!("Hello from spawn future local"); + while let Ok(msg) = rx.recv().await { + axis_label.set_text( + format!("X: {:>4} Y: {:>4}", msg.x_axis.unwrap_or("0".to_string()), msg.y_axis.unwrap_or("0".to_string())).as_str() + ); + button.set_label({ + if msg.connected { + ip_entry.set_sensitive(false); + port_entry.set_sensitive(false); + "Currently Connected" + } else { + ip_entry.set_sensitive(true); + port_entry.set_sensitive(true); + "Currently Disconnected" + } + }); + if msg.connected { + conn_status_label.set_css_classes(&["YesConnection"]); + button.set_css_classes(&["YesConnection"]); + } else { + conn_status_label.set_css_classes(&["NoConnection"]); + button.set_css_classes(&["NoConnection"]); + } + } + }), + ); + + // Create a window + let window = ApplicationWindow::builder() + .application(app) + .title("VCC Camera Controller") + .child(&main_box) + .build(); + + window.connect_close_request(move |_| { + do_run.store(false, std::sync::atomic::Ordering::SeqCst); + glib::Propagation::Proceed + }); + + // Present window + window.present(); +}