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::{ sync::{atomic::AtomicBool, Arc}, time::{Duration, Instant}, panic::{self, AssertUnwindSafe}, net::TcpStream, }; use websocket::client::{sync::Client, ClientBuilder}; use websocket::Message; use log::{info, error}; static MAX_RETRY_ATTEMPTS: u32 = 10; struct SocketState { pub ip: String, pub port: i32, pub socket: Option>, } impl SocketState { fn is_connected(&self) -> bool { self.socket.is_some() } fn close_websocket(&mut self) { if let Some(ref mut x) = self.socket { info!("closing websocket"); let _ = x.send_message(&Message::close()); let _ = x.shutdown(); self.socket = None; } } fn reconnect_websocket(&mut self) -> bool { if self.ip.is_empty() { self.socket = None; return false; } if let Ok(mut val) = ClientBuilder::new(format!("ws://{}:{}", &self.ip, self.port).as_str()) { if let Ok(val2) = val.connect_insecure() { self.socket = Some(val2); true } else { error!("couldn't connect websocket! : Step 1"); self.socket = None; false } } else { error!("couldn't connect websocket! : Step 2"); self.socket = None; false } } } struct JTState { pub socket: SocketState, pub try_reconnect: bool, pub retry_attempts: u32, pub curr_x: i32, pub curr_y: i32, pub last_update_time: Instant, } 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 state = JTState { socket: SocketState { ip: String::new(), port: 0, socket: None, }, try_reconnect: false, retry_attempts: 0, curr_x: 0, curr_y: 0, last_update_time: Instant::now(), }; loop { match rx.try_recv() { Ok(msg) => { state.socket.ip = msg.ip; state.socket.port = msg.port as i32; save_config(&AppState { ip: state.socket.ip.clone(), port: state.socket.port as u32, }); info!("Connecting to: ws://{}:{}", state.socket.ip, state.socket.port); if msg.start_websocket { if !state.socket.is_connected() { state.socket.reconnect_websocket(); } } else if state.socket.is_connected() { state.socket.close_websocket(); } } Err(async_channel::TryRecvError::Closed) => break, Err(async_channel::TryRecvError::Empty) => {} } if state.try_reconnect { if state.retry_attempts > MAX_RETRY_ATTEMPTS { state.try_reconnect = false; } if state.socket.is_connected() { state.try_reconnect = false; } else if state.socket.reconnect_websocket() { state.try_reconnect = false; state.retry_attempts = 0; } else { state.retry_attempts += 1; } } // catch unwind because some buttons on the joystick will panic the gilrs object match panic::catch_unwind(AssertUnwindSafe(|| { // get the next event, and if it is an axis we are interested in, update the // corresponding variable while let Some(evt) = gilrs.next_event().filter_ev(&UnknownSlayer {}, &mut gilrs) { match evt.event { gilrs::EventType::AxisChanged(gilrs::Axis::LeftStickY, val, _) => { state.curr_y = (val * 100.0) as i32; if state.curr_y > -10 && state.curr_y < 10 { state.curr_y = 0; } } gilrs::EventType::AxisChanged(gilrs::Axis::LeftStickX, val, _) => { state.curr_x = (val * 100.0) as i32; if state.curr_x > -10 && state.curr_x < 10 { state.curr_x = 0; } } _ => {} } } })) { Ok(_) => {} Err(_) => { info!("panic-causing event captured in gilrs event handler") } } if state.socket.is_connected() && Instant::now().duration_since(state.last_update_time) >= Duration::from_millis(150) { let mut message: String; if state.curr_y > 0 { message = format!("D{}:", state.curr_y); } else { message = format!("U{}:", state.curr_y.abs()); } if state.curr_x > 0 { message.push_str(&format!("R{}", state.curr_x)); } else { message.push_str(&format!("L{}", state.curr_x.abs())); } if let Some(ref mut websocket_tx) = state.socket.socket { if websocket_tx.send_message(&Message::text(message)).is_ok() { } else { state.socket.close_websocket(); } } state.last_update_time = Instant::now(); continue; } match tx.try_send(JoystickThreadUpdate { connected: state.socket.is_connected(), x_axis: Some(state.curr_x.to_string()), y_axis: Some(state.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) { info!("Exiting joystick thread"); break; } std::thread::sleep(Duration::from_millis(25)); } }