diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a9a4598..5feac7e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -60,7 +60,7 @@ jobs: name: windows_64-bit - os: windows-latest target: i686-pc-windows-msvc - name: windows_64-bit + name: windows_32-bit - os: macos-latest target: x86_64-apple-darwin name: macos_64-bit diff --git a/Cargo.lock b/Cargo.lock index a805da3..464a283 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2032,9 +2032,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "itoa", @@ -2045,15 +2045,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -2745,6 +2745,6 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab77e97b50aee93da431f2cee7cd0f43b4d1da3c408042f2d7d164187774f0a" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" diff --git a/Cargo.toml b/Cargo.toml index f56dba9..b6f4211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "dex_protect_osc_rs" -version = "0.1.1" +version = "0.1.2" edition = "2021" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -35,4 +36,8 @@ unicode-bom = "2" [features] file_dialog = ["dep:rfd"] -oscquery = [] \ No newline at end of file +oscquery = [] + +[profile.release] +strip = true +lto = true \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 12c2259..e3bf5c6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,9 +1,9 @@ use std::collections::VecDeque; -use std::fmt::{Debug, Display, Formatter}; -#[cfg(all(feature = "file_dialog", not(target_arch = "wasm32")))] +use std::convert::Infallible; +use std::fmt::{Debug, Formatter}; +use std::ops::IndexMut; use std::path::PathBuf; -use eframe::{Frame, Storage}; -use egui::{Context}; +use std::str::FromStr; use serde_derive::{Deserialize, Serialize}; use tokio::time::Instant; use crate::get_runtime; @@ -14,17 +14,23 @@ pub struct App<'a>{ logs_visible: bool, #[serde(skip)] collector:egui_tracing::EventCollector, - pub(crate) auto_connect_launch: bool, - pub(crate) ip:String, - pub(crate) unapplied_changes:bool, - pub(crate) path:String, + auto_connect_launch: bool, + ip:String, + path:String, #[cfg(all(feature = "file_dialog", not(target_arch = "wasm32")))] #[serde(skip)] file_picker_thread: Option>>, - pub(crate) osc_recv_port: u16, - pub(crate) osc_send_port: u16, + osc_recv_port: u16, + osc_send_port: u16, + osc_multiplexer_enabled: bool, + dex_protect_enabled: bool, + osc_multiplexer_rev_port: Vec, #[serde(skip)] - osc_thread: Option>>, + osc_multiplexer_port_popup: Option>>, + #[serde(skip)] + osc_thread: Option>>, + #[serde(skip)] + osc_join_set: Option>, osc_create_data: OscCreateData, #[serde(skip)] popups: VecDeque>>, @@ -36,12 +42,17 @@ impl<'a> Debug for App<'a>{ .field("collector",&self.collector) .field("auto_connect_launch",&self.auto_connect_launch) .field("ip", &self.ip) - .field("unapplied_changes", &self.unapplied_changes) .field("path", &self.path); #[cfg(all(feature = "file_dialog", not(target_arch = "wasm32")))] debug.field("file_picker_thread.is_some()", &self.file_picker_thread.is_some()); debug.field("osc_recv_port", &self.osc_recv_port) .field("osc_send_port", &self.osc_send_port) + .field("osc_multiplexer_enabled", &self.osc_multiplexer_enabled) + .field("dex_protect_enabled", &self.dex_protect_enabled) + .field("osc_multiplexer_rev_port", &self.osc_multiplexer_rev_port) + .field("osc_thread", &self.osc_thread) + .field("osc_join_set", &self.osc_join_set) + .field("osc_create_data", &self.osc_create_data) .field("popups.len()", &self.popups.len()) .finish() } @@ -53,18 +64,38 @@ impl<'a> Default for App<'a>{ collector:egui_tracing::EventCollector::new(), auto_connect_launch: true, ip:"127.0.0.1".to_string(), - unapplied_changes: false, path: "".to_string(), #[cfg(all(feature = "file_dialog", not(target_arch = "wasm32")))] file_picker_thread: None, osc_recv_port: crate::osc::OSC_RECV_PORT, osc_send_port: crate::osc::OSC_SEND_PORT, + osc_multiplexer_enabled: false, + dex_protect_enabled: true, + osc_multiplexer_rev_port: Vec::new(), + osc_multiplexer_port_popup: None, osc_thread: None, + osc_join_set: None, osc_create_data: OscCreateData::default(), popups: VecDeque::new(), } } } + +impl<'a> TryFrom<&App<'a>> for OscCreateData { + type Error = std::net::AddrParseError; + + fn try_from(value: &App<'a>) -> Result { + Ok(OscCreateData{ + ip: std::net::IpAddr::from_str(value.ip.as_str())?, + recv_port: value.osc_recv_port, + send_port: value.osc_send_port, + dex_protect_enabled: value.dex_protect_enabled, + path: PathBuf::from(&value.path), + osc_multiplexer_rev_port: if value.osc_multiplexer_enabled {value.osc_multiplexer_rev_port.clone()} else {Vec::new()}, + }) + } +} + impl<'a> App<'a> { /// Called once before the first frame. pub fn new(collector: egui_tracing::EventCollector, cc: &eframe::CreationContext<'_>) -> Self { @@ -112,48 +143,35 @@ impl<'a> App<'a> { ) { let error_string = error.to_string(); let label = label.into().clone(); - self.popups.push_front(popup_creator(title, move |ui| { + self.popups.push_front(popup_creator(title, move |_, ui| { ui.label(label.clone()); ui.label("Some developer information below:"); ui.label(&error_string); - ui.button("Close").clicked() })); } fn spawn_osc_from_creation_data(&mut self){ log::info!("Trying to connect to OSC on IP '{}'", self.osc_create_data.ip); - self.osc_thread = Some(tokio::spawn(start_osc(self.osc_create_data.clone()))); + let osc_create_data = self.osc_create_data.clone(); + self.osc_thread = Some(tokio::spawn(async move { + let mut js = crate::osc::create_and_start_osc(&osc_create_data).await?; + log::info!("Successfully connected to OSC and started all Handlers."); + loop{ + match js.join_next().await { + Some(Ok(_)) => { + log::error!("Joined a Task that should never finish. This should never happen.\nIs there a bug in the rust language, or is the developer just stupid?"); + }, + Some(Err(e)) => { + log::error!("Panic in OSC Thread: {}", e); + return Err(std::io::Error::new(std::io::ErrorKind::Other,e)) + }, + None => return Ok(()), + } + } + })); } -} - -async fn start_osc(osc_create_data: OscCreateData) -> Result<(),OSCError>{ - let osc = match crate::osc::Osc::new(&osc_create_data).await{ - Ok(v) => v, - Err(e) => { - return Err(OSCError::StdIo(e)); - } - }; - osc.listen().await; - - Ok(()) -} -#[derive(Debug)] -enum OSCError{ - StdIo(std::io::Error) -} -impl Display for OSCError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self{ - OSCError::StdIo(e) => write!(f,"OSCError::StdIo({})", e) - } - } -} - -impl std::error::Error for OSCError {} - -impl<'a> eframe::App for App<'a> { - fn update(&mut self, ctx: &Context, frame: &mut Frame) { + fn check_osc_thread(&mut self){ if let Some(osc_thread) = self.osc_thread.take() { if osc_thread.is_finished(){ match get_runtime().block_on(osc_thread){ @@ -162,10 +180,9 @@ impl<'a> eframe::App for App<'a> { let time = Instant::now(); self.popups.push_back(popup_creator( "OSC Thread Exited", - move |ui| { + move |_, ui| { ui.label("The OSC Thread (the one that communicates with VRChat) exited unexpectedly."); ui.label(format!("This happened {:.1} ago. (this updates only when you move your mouse or something changes)", time.elapsed().as_secs_f32())); - ui.button("Close").clicked() }) ) } @@ -182,154 +199,239 @@ impl<'a> eframe::App for App<'a> { self.osc_thread = Some(osc_thread); } } - egui::CentralPanel::default().show(ctx, |ui| { - ui.horizontal(|ui|{ - ui.label("IP:"); - ui.text_edit_singleline(&mut self.ip); - }); - ui.horizontal(|ui|{ - ui.label("OSC Receive Port:"); - ui.add(egui::DragValue::new(&mut self.osc_recv_port)); - if ui.button("Reset to Default").clicked() { - self.osc_recv_port = crate::osc::OSC_RECV_PORT; + } + fn dex_protect_ui(&mut self, ui:&mut egui::Ui){ + ui.heading("DexProtect:"); + ui.horizontal(|ui|{ + ui.label("Keys Folder: "); + #[cfg_attr(not(all(feature = "file_dialog", not(target_arch = "wasm32"))), allow(unused_variables))] + let resp = ui.add_enabled( + !self.has_file_picker_thread(), + egui::TextEdit::singleline(&mut self.path) + ); + #[cfg(all(feature = "file_dialog", not(target_arch = "wasm32")))] + { + if self.file_picker_thread.is_some(){ + resp.on_hover_text("A Dialogue to Pick a Folder is currently open."); } - }); - ui.horizontal(|ui|{ - ui.label("OSC Send Port:"); - ui.add(egui::DragValue::new(&mut self.osc_send_port)); - if ui.button("Reset to Default").clicked() { - self.osc_send_port = crate::osc::OSC_SEND_PORT; + } + #[cfg(not(all(feature = "file_dialog", not(target_arch = "wasm32"))))] + ui.label("(No Browse available. Copy and Paste the Path from your File Browser or type it in manually)"); + #[cfg(all(feature = "file_dialog", not(target_arch = "wasm32")))] + { + let mut resp = ui.add_enabled(self.file_picker_thread.is_none(), egui::Button::new("Browse")); + if !resp.enabled(){ + resp = resp.on_hover_text("A Dialogue to Pick a Folder is currently open. Please use that one."); } - }); - ui.horizontal(|ui|{ - ui.label("Keys Folder: "); - #[cfg_attr(not(all(feature = "file_dialog", not(target_arch = "wasm32"))), allow(unused_variables))] - let resp = ui.add_enabled( - !self.has_file_picker_thread(), - egui::TextEdit::singleline(&mut self.path) - ); - #[cfg(all(feature = "file_dialog", not(target_arch = "wasm32")))] - { - if self.file_picker_thread.is_some(){ - resp.on_hover_text("A Dialogue to Pick a Folder is currently open."); - } + if resp.clicked(){ + self.file_picker_thread = Some(get_runtime().spawn(async{ + rfd::AsyncFileDialog::new() + .pick_folder() + .await + .map(|f|f.path().to_path_buf()) + })); } - #[cfg(not(all(feature = "file_dialog", not(target_arch = "wasm32"))))] - ui.label("(No Browse available. Copy and Paste the Path from your File Browser or type it in manually)"); - #[cfg(all(feature = "file_dialog", not(target_arch = "wasm32")))] - - { - let mut resp = ui.add_enabled(self.file_picker_thread.is_none(), egui::Button::new("Browse")); - if !resp.enabled(){ - resp = resp.on_hover_text("A Dialogue to Pick a Folder is currently open. Please use that one."); - } - if resp.clicked(){ - self.file_picker_thread = Some(get_runtime().spawn(async{ - rfd::AsyncFileDialog::new() - .pick_folder() - .await - .map(|f|f.path().to_path_buf()) - })); - } - if let Some(file_picker_thread) = self.file_picker_thread.take(){ - if file_picker_thread.is_finished(){ - match get_runtime().block_on(file_picker_thread) { - Ok(Some(path)) => { - self.path = path.to_string_lossy().to_string(); - log::info!("Picked Folder: '{}' (potential replacements due to non UTF-8 characters) ", self.path); - }, - Ok(None) => log::info!("No Folder Picked."), - Err(e) => { - log::error!("Panic whist picking a Folder: {}", e); - self.handle_join_error(&e, "Critical Error whilst picking a Folder"); - } + if let Some(file_picker_thread) = self.file_picker_thread.take(){ + if file_picker_thread.is_finished(){ + match get_runtime().block_on(file_picker_thread) { + Ok(Some(path)) => { + self.path = path.to_string_lossy().to_string(); + log::info!("Picked Folder: '{}' (potential replacements due to non UTF-8 characters) ", self.path); + }, + Ok(None) => log::info!("No Folder Picked."), + Err(e) => { + log::error!("Panic whist picking a Folder: {}", e); + self.handle_join_error(&e, "Critical Error whilst picking a Folder"); } - }else{ - self.file_picker_thread = Some(file_picker_thread) } + }else{ + self.file_picker_thread = Some(file_picker_thread); } } - }); - - ui.horizontal(|ui|{ - if ui.button(if self.osc_thread.is_some() {"Reconnect"} else {"Connect"}).clicked() { - if let Some(osc_thread) = self.osc_thread.take(){ - log::info!("OSC Thread is already running and a Reconnect was requested. Aborting OSC thread."); - osc_thread.abort(); - log::info!("OSC Thread aborted"); - } - match OscCreateData::try_from(&*self) { - Ok(osc_create_data) => { - self.osc_create_data = osc_create_data; - self.spawn_osc_from_creation_data(); - }, - Err(e) => { - log::error!("\"{}\" is not a valid IP-Address. Rust error: \"{}\"",self.ip, e); - self.handle_display_popup(format!("\"{}\" is not a valid IP-Address", self.ip),&e,"Error Parsing IP-Address") + } + }); + ui.add_space(10.) + } + fn multiplexer_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Osc Multiplexer:"); + ui.label("All ports below will be forwarded to the Osc Send Port."); + ui.label("This allows you to use multiple Osc Send Applications at the same time."); + if ui.add_enabled(self.osc_multiplexer_port_popup.is_none(), egui::Button::new("Manage Ports")).clicked() { + self.osc_multiplexer_port_popup = Some(popup_creator_collapsible("Osc Multiplexer Ports:", true, |app, ui|{ + let mut i = 0; + while i < app.osc_multiplexer_rev_port.len(){ + ui.horizontal(|ui|{ + ui.label(format!("Osc Forward Port {}: ", i)); + ui.add(egui::DragValue::new(app.osc_multiplexer_rev_port.index_mut(i))); + if ui.button("Delete") + .on_hover_text("Delete this Port from the list, and replaces it with the last one.") + .clicked() + { + app.osc_multiplexer_rev_port.swap_remove(i); } - } + + }); + i+=1; + } + if ui.button("Add Port").clicked() { + app.osc_multiplexer_rev_port.push(0); } - if self.osc_thread.is_some() && ui.button("Disconnect").clicked() { - if let Some(osc_thread) = self.osc_thread.take(){ - log::info!("OSC Thread is already running and a Disconnect was requested. Aborting OSC thread."); - osc_thread.abort(); - log::info!("OSC Thread aborted"); + })); + } + ui.add_space(10.) + } + + fn osc_control_ui(&mut self, ui: &mut egui::Ui){ + ui.heading("Generic Osc Controls:"); + ui.horizontal(|ui|{ + ui.label("IP:"); + ui.text_edit_singleline(&mut self.ip); + }); + ui.horizontal(|ui|{ + ui.label("OSC Receive Port:"); + ui.add(egui::DragValue::new(&mut self.osc_recv_port)); + if ui.button("Reset to Default").clicked() { + self.osc_recv_port = crate::osc::OSC_RECV_PORT; + } + }); + ui.horizontal(|ui|{ + ui.label("OSC Send Port:"); + ui.add(egui::DragValue::new(&mut self.osc_send_port)); + if ui.button("Reset to Default").clicked() { + self.osc_send_port = crate::osc::OSC_SEND_PORT; + } + }); + ui.label("Please note that the Settings in the Ui will only be applied after you Reconnect/Connect."); + ui.horizontal(|ui|{ + if ui.button(if self.osc_thread.is_some() {"Reconnect"} else {"Connect"}).clicked() { + if let Some(osc_thread) = self.osc_thread.take(){ + log::info!("OSC Thread is already running and a Reconnect was requested. Aborting OSC thread."); + osc_thread.abort(); + log::info!("OSC Thread aborted"); + } + match OscCreateData::try_from(&*self) { + Ok(osc_create_data) => { + self.osc_create_data = osc_create_data; + self.spawn_osc_from_creation_data(); + }, + Err(e) => { + log::error!("\"{}\" is not a valid IP-Address. Rust error: \"{}\"",self.ip, e); + self.handle_display_popup(format!("\"{}\" is not a valid IP-Address", self.ip),&e,"Error Parsing IP-Address") } } - ui.checkbox(&mut self.auto_connect_launch, "Auto-Connect on Launch"); - if ui.button(if self.logs_visible {"Hide Logs"} else { "Show Logs"}).clicked() { - self.logs_visible = !self.logs_visible; + } + if self.osc_thread.is_some() && ui.button("Disconnect").clicked() { + if let Some(osc_thread) = self.osc_thread.take(){ + log::info!("OSC Thread is already running and a Disconnect was requested. Aborting OSC thread."); + osc_thread.abort(); + log::info!("OSC Thread aborted"); } - }); - if self.logs_visible { - egui::Resize::default() - .resizable(false) - .min_width(ctx.screen_rect().size().x-20.) - .min_height(f32::max(ctx.screen_rect().height()-170.,0.)) - .max_size(egui::vec2(ctx.screen_rect().width()-20.,f32::max(ctx.screen_rect().height()-170.,0.))) - .show(ui,|ui| - ui.add(egui_tracing::Logs::new(self.collector.clone())) - ); - // ui.add_space(10.); } + ui.checkbox(&mut self.auto_connect_launch, "Auto-Connect on Launch"); }); + ui.add_space(10.); + } +} - let mut i = 0; - while i < self.popups.len() { - let popup = &mut self.popups[i]; - if popup(ctx, frame) { - self.popups.remove(i); - } else { - i += 1; +impl<'a> eframe::App for App<'a> { + fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + self.check_osc_thread(); + egui::CentralPanel::default().show(ctx, |ui| { + //create immutable copies + let dex_protect_enabled = self.dex_protect_enabled; + let osc_multiplexer_enabled = self.osc_multiplexer_enabled; + let logs_visible = self.logs_visible; + let mut strip_builder = egui_extras::StripBuilder::new(ui); + if dex_protect_enabled { + strip_builder = strip_builder.size(egui_extras::Size::exact(50.)); + } + if osc_multiplexer_enabled { + strip_builder = strip_builder.size(egui_extras::Size::exact(90.)); + } + strip_builder = strip_builder.size(egui_extras::Size::exact(130.)) + .size(egui_extras::Size::exact(25.)); + if logs_visible { + strip_builder = strip_builder.size(egui_extras::Size::remainder()); + } + strip_builder.vertical(|mut strip| { + if dex_protect_enabled { + strip.cell(|ui|{ + self.dex_protect_ui(ui); + }); + } + if osc_multiplexer_enabled { + strip.cell(|ui|{ + self.multiplexer_ui(ui); + }); + } + strip.cell(|ui|{ + self.osc_control_ui(ui); + }); + strip.cell(|ui| { + ui.horizontal(|ui|{ + if ui.button(if self.logs_visible {"Hide Logs"} else { "Show Logs"}).clicked() { + self.logs_visible = !self.logs_visible; + } + ui.checkbox(&mut self.dex_protect_enabled, "Enable DexProtectOSC"); + ui.checkbox(&mut self.osc_multiplexer_enabled, "Enable Osc Multiplexer (allows for multiple Osc send applications) "); + }); + }); + if logs_visible { + strip.cell(|ui|{ + ui.add(egui_tracing::Logs::new(self.collector.clone())); + }); + } + }); + + }); + + if let Some(mut popup) = self.osc_multiplexer_port_popup.take() { + if popup(self, ctx, frame) { + self.osc_multiplexer_port_popup = Some(popup); } } + self.popups = core::mem::take(&mut self.popups).into_iter().filter_map(|mut popup|{ + if popup(self, ctx, frame) { + Some(popup) + }else{ + None + } + }).collect(); } - fn save(&mut self, storage: &mut dyn Storage) { + fn save(&mut self, storage: &mut dyn eframe::Storage) { eframe::set_value(storage,eframe::APP_KEY, self) } } -type PopupFunc<'a> = dyn Fn(&'_ egui::Context, &'_ mut eframe::Frame) -> bool + 'a; +type PopupFunc<'a> = dyn FnMut(&'_ mut App,&'_ egui::Context, &'_ mut eframe::Frame) -> bool + 'a; fn get_id() -> u64 { static ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst) } + fn popup_creator<'a>( title: impl Into + 'a, - add_content: impl Fn(&mut egui::Ui) -> bool + 'a, + add_content: impl FnMut(&mut App, &mut egui::Ui) + 'a, +) -> Box> { + popup_creator_collapsible(title, false, add_content) +} + +fn popup_creator_collapsible<'a>( + title: impl Into + 'a, + collapsible: bool, + mut add_content: impl FnMut(&mut App, &mut egui::Ui) + 'a, ) -> Box> { let title = title.into(); let id = get_id(); - Box::new(move |ctx: &'_ egui::Context, _: &'_ mut eframe::Frame| { - let mut clicked = false; + let mut open = true; + Box::new(move |app:&'_ mut App,ctx: &'_ egui::Context, _: &'_ mut eframe::Frame| { egui::Window::new(title.clone()) .resizable(false) - .collapsible(false) + .collapsible(collapsible) + .open(&mut open) .id(egui::Id::new(id)) - .show(ctx, |ui| { - clicked = add_content(ui); - }); - clicked + .show(ctx, |ui|add_content(app,ui)); + open }) } diff --git a/src/osc.rs b/src/osc.rs index 6ff30e1..ad5be9e 100644 --- a/src/osc.rs +++ b/src/osc.rs @@ -1,345 +1,28 @@ -use std::collections::VecDeque; -use async_recursion::async_recursion; -use std::io; -use std::net::{AddrParseError, IpAddr, Ipv4Addr}; -use std::ops::{Index, Shr}; +use std::convert::Infallible; +use std::net::{IpAddr, Ipv4Addr}; use std::path::PathBuf; -use std::str::FromStr; -use std::time::SystemTime; -use rosc::{OscBundle, OscMessage, OscPacket, OscType}; -use serde_derive::{Deserialize, Serialize}; -use tokio::net::UdpSocket; -use unicode_bom::Bom; -use crate::app::App; - -pub const OSC_RECV_PORT:u16 = 9001; -pub const OSC_SEND_PORT:u16 = 9000; -pub(crate) struct Osc { - bundles: Vec, - osc_recv:UdpSocket, - osc_send:UdpSocket, - path:PathBuf, -} - -impl Osc{ - pub async fn new(osc_create_data: &OscCreateData) -> io::Result { - let osc_send = match bind_and_connect_udp(osc_create_data.ip, 0, osc_create_data.send_port,"send").await{ - Ok(v) => v, - Err(e) => { - log::warn!("Failed to Bind and/or connect the OSC UDP send socket: {}", e); - Err(e)? - } - }; - log::info!("About to Bind OSC UDP receive Socket to {}:{}", osc_create_data.ip,osc_create_data.recv_port); - let osc_recv = match UdpSocket::bind((osc_create_data.ip,osc_create_data.recv_port)).await{ - Ok(v) => v, - Err(e) => { - log::warn!("Failed to Bind and/or connect the OSC UDP receive socket: {}", e); - Err(e)? - } - }; - log::info!("Bound OSC UDP receive Socket."); - - Ok(Osc{ - bundles: Vec::new(), - osc_send, - osc_recv, - path: osc_create_data.path.clone() - }) - } - - pub async fn listen(mut self) -> ! { - loop{ - self.check_osc_bundles().await; - let mut buf = [0u8;8192]; - match self.osc_recv.recv(&mut buf).await { - Err(e) => { - log::error!("Error receiving udp packet. Skipping Packet: {}",e); - continue; - } - Ok(size) => { - #[cfg(debug_assertions)] - log::trace!("Received UDP Packet with size {} ",size); - match rosc::decoder::decode_udp(&buf[..size]) { - Err(e) => { - log::error!("Error decoding udp packet into an OSC Packet: {}", e); - #[cfg(debug_assertions)] - log::trace!("Packet contents were: {:#X?}",&buf[..size]); - continue; - } - Ok((_, packet)) => self.handle_packet(packet).await - } - } - }; - - - } - } - async fn send_message(&self, message: &OscPacket){ - #[cfg(debug_assertions)] - log::trace!("Sending OSC Message: {:#?}", message); - match rosc::encoder::encode(message) { - Ok(v) => match self.osc_send.send(v.as_slice()).await { - Ok(_) => return, - Err(e) => log::error!("Failed to send a OSC Message: {}, Encoded Packet was: {:#x?}, Osc Message was: {:#?}",e,v.as_slice(), message) - }, - Err(e) => log::error!("Failed to encode a OSC Message: {}, Packet was: {:#?}",e, message) - } - } +use std::sync::Arc; - async fn check_osc_bundles(&mut self){ - let mut i = 0; - while i < self.bundles.len() { - let element = self.bundles.index(i); - if SystemTime::from(element.timetag) < SystemTime::now() { - let content = self.bundles.swap_remove(i).content; - self.apply_packets(content).await; - }else{ - i+=1; - } - } - } - - async fn apply_packets(&mut self, packets:Vec){ - for i in packets{ - self.handle_packet(i).await; - } - } - - #[async_recursion] - async fn handle_packet(&mut self, packet: OscPacket){ - match packet { - OscPacket::Message(msg) => { - #[cfg(debug_assertions)] - log::trace!("Got a OSC Packet: {}: {:?}", msg.addr, msg.args); - self.handle_message(msg).await; - } - OscPacket::Bundle(bundle) => { - if bundle.timetag.seconds == 0 && bundle.timetag.fractional == 1{ - self.apply_packets(bundle.content).await; - return; - } - log::debug!("Got a OSC Bundle to be applied in {}.{}s {:?}", bundle.timetag.seconds, bundle.timetag.fractional, bundle.timetag.fractional); - self.bundles.push(bundle); - } - } - } - - async fn handle_message(&self, message: OscMessage){ - if message.addr.eq_ignore_ascii_case("/avatar/change") { - let mut id = None; - for i in &message.args{ - match i { - OscType::String(s) => { - if id.is_none(){ - id = Some(s); - }else{ - unrecognized_avatar_change(&message.args); - return; - } - } - _ => { - unrecognized_avatar_change(&message.args); - } - } - } - if let Some(id) = id{ - self.handle_avatar_change(id).await; - }else{ - log::error!("No avatar id was found for the '/avatar/change' message. This is unexpected and might be a change to VRChat's OSC messages.") - } - } - } - - async fn handle_avatar_change(&self, id: &String) { - let mut path = self.path.clone(); - path.push(id); - path.set_extension("key"); - match tokio::fs::read(path).await{ - Ok(v) => { - let mut v = match vecu8_to_str(v){ - Some(v) => v, - None => { - log::error!("Failed to decode the Avatar id '{}' Key file. Refusing to unlock.", id); - return; - } - }; - #[cfg(debug_assertions)] - log::debug!("Decoded Avatar id '{}' Key file: '{}'", id, v); - let mut key = Vec::new(); - v = v.replace(",","."); - let split:Vec<&str> = v.split("|").collect(); - let len = if split.len()%2 == 0 { - split.len() - }else{ - log::error!("Found an uneven amount of keys in the Avatar id '{}' key file.\n This is highly unusual and suggests corruption in the key file. \n You should suggest reporting this in the Discord for DexProtect.\n All bets are off from here on out, if unlocking will actually work.", id); - split.len()-1 - }; - let mut i = 0; - while i < len { - let float = split.index(i); - log::trace!("Decoding float: {}", float); - let whole:u32; - let part:u32; - let part_digits:u32; - if let Some(index) = float.find("."){ - let (whole_str, part_str) = float.split_at(index); - let mut part_string = part_str.to_string(); - part_string.remove(0); - log::trace!("Decoding float: {}, whole: {}, part:{}", float,whole_str, part_string); - whole = match decode_number(whole_str, id){ - Some(v) => v, - None => return - }; - part = match decode_number(part_string.as_str(), id){ - Some(v) => v, - None => return - }; - part_digits = part_string.len() as u32; - }else { - whole = match decode_number(float, id){ - Some(v) => v, - None => return - }; - part = 0; - part_digits = 0; - } - let amount = whole as f32 + part as f32/(10.0f32.powf(part_digits as f32)); - key.push(OscPacket::Message(OscMessage{ - addr: format!("/avatar/parameters/{}", split.index(i+1)), - args: vec![OscType::Float(amount)], - })); - i+=2; - } - self.send_message(&OscPacket::Bundle(OscBundle{ - timetag: rosc::OscTime{ - seconds: 0, - fractional: 1 - }, - content: key - })).await; - log::info!("Avatar Change Detected to Avatar id '{}'. Key was detected, has been decoded and the Avatar has been Unlocked.", id); - } - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound{ - log::info!("No key detected for avatar ID {}, not unlocking.\nAssuming that the following error actually means the file doesn't exist and not just a directory along the way:\n {}", id, e); - return; - } - log::error!("Failed to read the Avatar id '{}' from the Avatar Folder: {}.", id, e); - } - } +use serde_derive::{Deserialize, Serialize}; - } -} +pub use sender::OscSender; -fn decode_number(number:&str, id:&str) -> Option { - match u32::from_str(number){ - Ok(v) => Some(v), - Err(e) => { - log::error!("Error whilst decoding part of the Key for the Avatar id '{}': {}.\n Refusing to unlock.", id, e); - None - } - } -} -fn vecu8_to_str(v:Vec) -> Option { - let bom = unicode_bom::Bom::from(v.as_slice()); - match bom { - Bom::Null => { - log::debug!("No BOM Detected. Assuming UTF-8."); - let mut vec_deque = VecDeque::from(v); - vec_deque.pop_front(); - vec_deque.pop_front(); - vec_deque.pop_front(); - match String::from_utf8(vec_deque.into()) { - Ok(v) => Some(v), - Err(_) => None, - } - } - Bom::Bocu1 => None, - Bom::Gb18030 => None, - Bom::Scsu => None, - Bom::UtfEbcdic => None, - Bom::Utf1 => None, - Bom::Utf7 => None, - Bom::Utf8 => { - log::debug!("Detected UTF-8 file."); - let mut vec_deque = VecDeque::from(v); - vec_deque.pop_front(); - vec_deque.pop_front(); - vec_deque.pop_front(); - match String::from_utf8(vec_deque.into()) { - Ok(v) => Some(v), - Err(_) => None, - } - } - Bom::Utf16Be => { - log::debug!("Detected UTF-16Be file."); - let mut utf16_buf = VecDeque::from(vecu8_to_vecu16(v, true)); - utf16_buf.pop_front(); - log::debug!("Decoded {} u16 values.", utf16_buf.len()); - utf16_buf_to_str(utf16_buf.into()) - } - Bom::Utf16Le => { - log::debug!("Detected UTF-16Le file."); - let mut utf16_buf = VecDeque::from(vecu8_to_vecu16(v,false)); - utf16_buf.pop_front(); - log::debug!("Decoded {} u16 values.", utf16_buf.len()); - utf16_buf_to_str(utf16_buf.into()) - } - Bom::Utf32Be => None, - Bom::Utf32Le => None, - } -} -fn vecu8_to_vecu16(v:Vec, be:bool) -> Vec{ - log::debug!("Got {} bytes.", v.len()); - let mut utf16buf:Vec = Vec::new(); - let mut i = 0; - let len = if v.len()%2 == 0 { - v.len() - } else { - log::debug!("Uneven amount of bytes read from key file."); - v.len()-1 - }; - while i < len{ - utf16buf.push(if be {(*v.index(i) as u16).shr(8) | (*v.index(i+1) as u16)} else {(*v.index(i+1) as u16).shr(8) | (*v.index(i) as u16)}); - i+=2; - } - if len != v.len() { - log::info!("Reappending last byte."); - utf16buf.push(*v.index(len) as u16); - } - log::debug!("Converted to {} u16 values.", utf16buf.len()); - utf16buf -} -fn utf16_buf_to_str(v:Vec) -> Option{ - let mut string = String::new(); - for i in char::decode_utf16(v){ - match i { - Ok(v)=>string.push(v), - Err(_) => return None, - } - } - return Some(string); -} -fn unrecognized_avatar_change(arg:&Vec){ - log::error!("Received a OSC Message with the address /avatar/change but the first argument was not a string.\n This is unexpected and there might have been a change to VRChat's OSC messages.\n Extraneous Argument: {:#?}", arg); -} +mod sender; +mod dex; +mod multiplexer; -async fn bind_and_connect_udp(ip:IpAddr, bind_port:u16, connect_port:u16, way:&str) -> io::Result { - log::info!("About to Bind OSC UDP {} Socket on port {}", way,bind_port); - let udp_sock = UdpSocket::bind((ip,bind_port)).await?; - log::info!("Bound OSC UDP {} Socket. About to connect to {}:{}.", way,ip,connect_port); - udp_sock.connect((ip,connect_port)).await?; - log::info!("Connected OSC UDP {} Socket to {}:{}.", way,ip,connect_port); - Ok(udp_sock) -} +pub const OSC_RECV_PORT:u16 = 9001; +pub const OSC_SEND_PORT:u16 = 9000; +const OSC_RECV_BUFFER_SIZE:usize = 8192; #[derive(Debug, Clone,Serialize,Deserialize)] -pub(crate) struct OscCreateData { +pub struct OscCreateData { pub ip: IpAddr, pub recv_port:u16, pub send_port:u16, + pub dex_protect_enabled:bool, pub path: PathBuf, + pub osc_multiplexer_rev_port: Vec, } impl Default for OscCreateData { @@ -348,21 +31,44 @@ impl Default for OscCreateData { ip: IpAddr::V4(Ipv4Addr::LOCALHOST), recv_port: OSC_RECV_PORT, send_port: OSC_SEND_PORT, - path: PathBuf::new() + dex_protect_enabled: true, + path: PathBuf::new(), + osc_multiplexer_rev_port: Vec::new(), } } } - -impl<'a> TryFrom<&App<'a>> for OscCreateData { - type Error = AddrParseError; - - fn try_from(value: &App<'a>) -> Result { - Ok(OscCreateData{ - ip: IpAddr::from_str(value.ip.as_str())?, - recv_port: value.osc_recv_port, - send_port: value.osc_send_port, - path: PathBuf::from(&value.path) - }) +pub async fn create_and_start_osc(osc_create_data: &OscCreateData) -> std::io::Result> { + let osc = match OscSender::new(osc_create_data.ip, osc_create_data.send_port).await { + Ok(v) => Arc::new(v), + Err(e) => { + log::error!("Failed to create OSC Sender: {}", e); + return Err(e) + } + }; + log::info!("Created OSC Sender."); + let dex_osc = if osc_create_data.dex_protect_enabled{ + match dex::DexOsc::new(osc_create_data, osc.clone()).await { + Ok(v) => { + log::info!("Created DexProtectOsc Handler."); + Some(v) + }, + Err(e) => { + log::error!("Failed to create DexOsc: {}", e); + return Err(e) + } + } + }else{ + None + }; + let multiplexer = multiplexer::MultiplexerOsc::new(osc.clone(), osc_create_data.ip, osc_create_data.osc_multiplexer_rev_port.clone()).await?; + log::info!("Created OSC Multiplexer (if any)."); + let mut js = tokio::task::JoinSet::new(); + if let Some(dex_osc) = dex_osc { + dex_osc.listen(&mut js); + log::info!("Started DexProtectOsc Handler."); } + multiplexer.listen(&mut js); + log::info!("Started OSC Multiplexer (if any)."); + Ok(js) } \ No newline at end of file diff --git a/src/osc/dex.rs b/src/osc/dex.rs new file mode 100644 index 0000000..c2436b9 --- /dev/null +++ b/src/osc/dex.rs @@ -0,0 +1,305 @@ +use std::collections::VecDeque; +use std::convert::Infallible; +use std::ops::{Index, Shr}; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; +use std::time::SystemTime; +use rosc::{OscBundle, OscMessage, OscPacket, OscType}; +use tokio::net::UdpSocket; +use unicode_bom::Bom; +use super::OscSender; +use super::OscCreateData; + +pub(super) struct DexOsc { + bundles: Vec, + osc_recv: UdpSocket, + path:PathBuf, + osc: Arc, +} + +impl DexOsc { + pub async fn new(osc_create_data: &OscCreateData, osc:Arc) -> std::io::Result { + log::info!("About to Bind OSC UDP receive Socket to {}:{}", osc_create_data.ip,osc_create_data.recv_port); + let osc_recv = match UdpSocket::bind((osc_create_data.ip,osc_create_data.recv_port)).await{ + Ok(v) => v, + Err(e) => { + log::warn!("Failed to Bind and/or connect the OSC UDP receive socket: {}", e); + Err(e)? + } + }; + log::info!("Bound OSC UDP receive Socket."); + + Ok(DexOsc { + bundles: Vec::new(), + osc_recv, + path: osc_create_data.path.clone(), + osc + }) + } + + pub fn listen(mut self, js:&mut tokio::task::JoinSet) { + js.spawn(async move { + loop { + self.check_osc_bundles().await; + let mut buf = [0u8; super::OSC_RECV_BUFFER_SIZE]; + match self.osc_recv.recv(&mut buf).await { + Err(e) => { + log::error!("Error receiving udp packet. Skipping Packet: {}",e); + continue; + } + Ok(size) => { + #[cfg(debug_assertions)] + log::trace!("Received UDP Packet with size {} ",size); + match rosc::decoder::decode_udp(&buf[..size]) { + Err(e) => { + log::error!("Error decoding udp packet into an OSC Packet: {}", e); + #[cfg(debug_assertions)] + log::trace!("Packet contents were: {:#X?}",&buf[..size]); + continue; + } + Ok((_, packet)) => self.handle_packet(packet).await + } + } + }; + } + }); + } + + async fn check_osc_bundles(&mut self){ + let mut i = 0; + while i < self.bundles.len() { + let element = &self.bundles[i]; + if SystemTime::from(element.timetag) < SystemTime::now() { + let content = self.bundles.swap_remove(i).content; + self.apply_packets(content).await; + }else{ + i+=1; + } + } + } + + async fn apply_packets(&mut self, packets:Vec){ + for i in packets{ + self.handle_packet(i).await; + } + } + + #[async_recursion::async_recursion] + async fn handle_packet(&mut self, packet: OscPacket){ + match packet { + OscPacket::Message(msg) => { + #[cfg(debug_assertions)] + log::trace!("Got a OSC Packet: {}: {:?}", msg.addr, msg.args); + self.handle_message(msg).await; + } + OscPacket::Bundle(bundle) => { + if bundle.timetag.seconds == 0 && bundle.timetag.fractional == 1{ + self.apply_packets(bundle.content).await; + return; + } + log::debug!("Got a OSC Bundle to be applied in {}.{}s {:?}", bundle.timetag.seconds, bundle.timetag.fractional, bundle.timetag.fractional); + self.bundles.push(bundle); + } + } + } + + async fn handle_message(&self, message: OscMessage){ + if message.addr.eq_ignore_ascii_case("/avatar/change") { + let mut id = None; + for i in &message.args{ + match i { + OscType::String(s) => { + if id.is_none(){ + id = Some(s); + }else{ + unrecognized_avatar_change(&message.args); + return; + } + } + _ => { + unrecognized_avatar_change(&message.args); + } + } + } + if let Some(id) = id{ + self.handle_avatar_change(id).await; + }else{ + log::error!("No avatar id was found for the '/avatar/change' message. This is unexpected and might be a change to VRChat's OSC messages.") + } + } + } + + async fn handle_avatar_change(&self, id: &String) { + let mut path = self.path.clone(); + path.push(id); + path.set_extension("key"); + match tokio::fs::read(path).await{ + Ok(v) => { + let mut v = match vecu8_to_str(v){ + Some(v) => v, + None => { + log::error!("Failed to decode the Avatar id '{}' Key file. Refusing to unlock.", id); + return; + } + }; + #[cfg(debug_assertions)] + log::debug!("Decoded Avatar id '{}' Key file: '{}'", id, v); + let mut key = Vec::new(); + v = v.replace(",","."); + let split:Vec<&str> = v.split("|").collect(); + let len = if split.len()%2 == 0 { + split.len() + }else{ + log::error!("Found an uneven amount of keys in the Avatar id '{}' key file.\n This is highly unusual and suggests corruption in the key file. \n You should suggest reporting this in the Discord for DexProtect.\n All bets are off from here on out, if unlocking will actually work.", id); + split.len()-1 + }; + let mut i = 0; + while i < len { + let float = split[i]; + log::trace!("Decoding float: {}", float); + let whole:u32; + let part:u32; + let part_digits:u32; + if let Some(index) = float.find("."){ + let (whole_str, part_str) = float.split_at(index); + let mut part_string = part_str.to_string(); + part_string.remove(0); + log::trace!("Decoding float: {}, whole: {}, part:{}", float,whole_str, part_string); + whole = match decode_number(whole_str, id){ + Some(v) => v, + None => return + }; + part = match decode_number(part_string.as_str(), id){ + Some(v) => v, + None => return + }; + part_digits = part_string.len() as u32; + }else { + whole = match decode_number(float, id){ + Some(v) => v, + None => return + }; + part = 0; + part_digits = 0; + } + let amount = whole as f32 + part as f32/(10.0f32.powf(part_digits as f32)); + key.push(OscPacket::Message(OscMessage{ + addr: format!("/avatar/parameters/{}", split[i+1]), + args: vec![OscType::Float(amount)], + })); + i+=2; + } + self.osc.send_message_with_logs(&OscPacket::Bundle(OscBundle{ + timetag: rosc::OscTime{ + seconds: 0, + fractional: 1 + }, + content: key + })).await; + log::info!("Avatar Change Detected to Avatar id '{}'. Key was detected, has been decoded and the Avatar has been Unlocked.", id); + } + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound{ + log::info!("No key detected for avatar ID {}, not unlocking.\nAssuming that the following error actually means the file doesn't exist and not just a directory along the way:\n {}", id, e); + return; + } + log::error!("Failed to read the Avatar id '{}' from the Avatar Folder: {}.", id, e); + } + } + + } +} + +fn decode_number(number:&str, id:&str) -> Option { + match u32::from_str(number){ + Ok(v) => Some(v), + Err(e) => { + log::error!("Error whilst decoding part of the Key for the Avatar id '{}': {}.\n Refusing to unlock.", id, e); + None + } + } +} +fn vecu8_to_str(v:Vec) -> Option { + let bom = unicode_bom::Bom::from(v.as_slice()); + match bom { + Bom::Null => { + log::debug!("No BOM Detected. Assuming UTF-8."); + let mut vec_deque = VecDeque::from(v); + vec_deque.pop_front(); + vec_deque.pop_front(); + vec_deque.pop_front(); + match String::from_utf8(vec_deque.into()) { + Ok(v) => Some(v), + Err(_) => None, + } + } + Bom::Bocu1 => None, + Bom::Gb18030 => None, + Bom::Scsu => None, + Bom::UtfEbcdic => None, + Bom::Utf1 => None, + Bom::Utf7 => None, + Bom::Utf8 => { + log::debug!("Detected UTF-8 file."); + let mut vec_deque = VecDeque::from(v); + vec_deque.pop_front(); + vec_deque.pop_front(); + vec_deque.pop_front(); + match String::from_utf8(vec_deque.into()) { + Ok(v) => Some(v), + Err(_) => None, + } + } + Bom::Utf16Be => { + log::debug!("Detected UTF-16Be file."); + let mut utf16_buf = VecDeque::from(vecu8_to_vecu16(v, true)); + utf16_buf.pop_front(); + log::debug!("Decoded {} u16 values.", utf16_buf.len()); + utf16_buf_to_str(utf16_buf.into()) + } + Bom::Utf16Le => { + log::debug!("Detected UTF-16Le file."); + let mut utf16_buf = VecDeque::from(vecu8_to_vecu16(v,false)); + utf16_buf.pop_front(); + log::debug!("Decoded {} u16 values.", utf16_buf.len()); + utf16_buf_to_str(utf16_buf.into()) + } + Bom::Utf32Be => None, + Bom::Utf32Le => None, + } +} +fn vecu8_to_vecu16(v:Vec, be:bool) -> Vec{ + log::debug!("Got {} bytes.", v.len()); + let mut utf16buf:Vec = Vec::new(); + let mut i = 0; + let len = if v.len()%2 == 0 { + v.len() + } else { + log::debug!("Uneven amount of bytes read from key file."); + v.len()-1 + }; + while i < len{ + utf16buf.push(if be {(*v.index(i) as u16).shr(8) | (*v.index(i+1) as u16)} else {(*v.index(i+1) as u16).shr(8) | (*v.index(i) as u16)}); + i+=2; + } + if len != v.len() { + log::info!("Reappending last byte."); + utf16buf.push(*v.index(len) as u16); + } + log::debug!("Converted to {} u16 values.", utf16buf.len()); + utf16buf +} +fn utf16_buf_to_str(v:Vec) -> Option{ + let mut string = String::new(); + for i in char::decode_utf16(v){ + match i { + Ok(v)=>string.push(v), + Err(_) => return None, + } + } + return Some(string); +} +fn unrecognized_avatar_change(arg:&Vec){ + log::error!("Received a OSC Message with the address /avatar/change but the first argument was not a string.\n This is unexpected and there might have been a change to VRChat's OSC messages.\n Extraneous Argument: {:#?}", arg); +} diff --git a/src/osc/multiplexer.rs b/src/osc/multiplexer.rs new file mode 100644 index 0000000..83f6fed --- /dev/null +++ b/src/osc/multiplexer.rs @@ -0,0 +1,76 @@ +use std::convert::Infallible; +use std::net::IpAddr; +use std::sync::Arc; +use tokio::net::UdpSocket; +use super::OscSender; + +pub(super) struct MultiplexerOsc { + osc: Arc, + forward_sockets: Vec, +} + +impl MultiplexerOsc{ + pub async fn new(osc: Arc, ip: IpAddr, mut forward_ports: Vec) -> std::io::Result { + forward_ports.dedup(); + let mut forward_sockets = Vec::new(); + let mut js = tokio::task::JoinSet::new(); + for port in forward_ports { + js.spawn(async move { + log::info!("About to Bind OSC UDP receive Socket to {}:{}", ip,port); + match UdpSocket::bind((ip,port)).await{ + Ok(v) => Ok(v), + Err(e) => { + log::warn!("Failed to Bind and/or connect the OSC UDP receive socket: {}", e); + Err(e) + } + } + }); + } + loop{ + match js.join_next().await{ + Some(Ok(Ok(v))) => forward_sockets.push(v), + Some(Ok(Err(err))) => { + log::warn!("Failed to Bind the OSC UDP receive socket: {}", err); + return Err(err) + } + Some(Err(e)) => { + log::error!("Critical Error while binding OSC UDP receive socket: {}", e); + return Err(std::io::Error::new(std::io::ErrorKind::Other, e)) + } + None => break, + } + } + Ok(Self{ + osc, + forward_sockets, + }) + } + + pub fn listen(self,js:&mut tokio::task::JoinSet) { + for socket in self.forward_sockets { + let cloned_osc = self.osc.clone(); + js.spawn(async move { + loop{ + let mut buf = [0u8; super::OSC_RECV_BUFFER_SIZE]; + match socket.recv(&mut buf).await { + Ok(size) => { + match cloned_osc.send_raw_packet(&buf[..size]).await { + Ok(sent_size) => { + if size != sent_size { + log::warn!("Received OSC Packet to be forwarded with size {} but the sent/forwarded OSC Packet had size {}", sent_size, size); + } + }, + Err(e) => { + log::error!("Failed to send OSC Packet: {}", e); + } + } + } + Err(e) => { + log::error!("Failed to receive OSC Packet (to be forwarded): {}. Skipping Packet.", e); + } + } + } + }); + } + } +} \ No newline at end of file diff --git a/src/osc/sender.rs b/src/osc/sender.rs new file mode 100644 index 0000000..d0f40b4 --- /dev/null +++ b/src/osc/sender.rs @@ -0,0 +1,90 @@ +use std::net::IpAddr; +use tokio::net::UdpSocket; + +///Allows for sending OSC Messages +pub struct OscSender { + osc_send:UdpSocket, +} +async fn bind_and_connect_udp(ip:IpAddr, bind_port:u16, connect_port:u16, way:&str) -> std::io::Result { + log::info!("About to Bind OSC UDP {} Socket on port {}", way,bind_port); + let udp_sock = UdpSocket::bind((ip,bind_port)).await?; + log::info!("Bound OSC UDP {} Socket. About to connect to {}:{}.", way,ip,connect_port); + udp_sock.connect((ip,connect_port)).await?; + log::info!("Connected OSC UDP {} Socket to {}:{}.", way,ip,connect_port); + Ok(udp_sock) +} +impl OscSender { + /// Creates a new OSC Sender. + /// This will bind a UDP Socket to a random port and connect it to the specified port on the specified ip. + /// The binding and the connection can both fail, so this function returns a Result. + pub async fn new(ip:IpAddr,port:u16) -> Result{ + let osc_send = match bind_and_connect_udp(ip, 0, port,"send").await{ + Ok(v) => v, + Err(e) => { + log::warn!("Failed to Bind and/or connect the OSC UDP send socket: {}", e); + Err(e)? + } + }; + Ok(Self{ + osc_send, + }) + } + /// Sends a OSC Message and returns the amount of bytes sent if successful or any errors. + pub async fn send_message_no_logs(&self, message: &rosc::OscPacket) -> Result{ + + let message = rosc::encoder::encode(message)?; + match self.osc_send.send(message.as_slice()).await { + Ok(v) => Ok(v), + Err(err) => Err(OscSendError::Io(err, message)), + } + } + + /// Sends a OSC Message via {@link #send_message_no_logs}. + /// If there are any errors, they will be logged. + /// If debug assertions are enabled, the sending attempt of the message will be logged and the successful sending will also be logged. + pub async fn send_message_with_logs(&self, message: &rosc::OscPacket) { + #[cfg(debug_assertions)] + log::trace!("Sending OSC Message: {:#?}", message); + match self.send_message_no_logs(message).await { + #[cfg(not(debug_assertions))] + Ok(_)=>{}, + #[cfg(debug_assertions)] + Ok(bytes) => { + log::debug!("Sent the following OSC Message with {} bytes:{:#?}",bytes,message); + } + Err(OscSendError::Io(err, v)) => { + log::error!("Failed to send a OSC Message: {}, Encoded Packet was: {:#x?}, Osc Message was: {:#?}",err,v.as_slice(), message); + }, + Err(OscSendError::OscError(err)) => { + log::error!("Failed to encode a OSC Message: {}, Packet was: {:#?}",err, message); + } + }; + } + + pub async fn send_raw_packet(&self, packet: &[u8]) -> std::io::Result{ + self.osc_send.send(packet).await + } + +} + +#[derive(Debug)] +pub enum OscSendError{ + Io(std::io::Error, Vec), + OscError(rosc::OscError), +} + +impl From for OscSendError{ + fn from(value: rosc::OscError) -> Self { + OscSendError::OscError(value) + } +} + +impl std::fmt::Display for OscSendError{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self{ + OscSendError::Io(v,_) => write!(f,"OscSendError::Io({})",v), + OscSendError::OscError(v) => write!(f,"OscSendError::Io({})",v), + } + } +} +impl std::error::Error for OscSendError {} \ No newline at end of file