From be1b1ad11575ac76ade6f61542435b5f4ece1b41 Mon Sep 17 00:00:00 2001 From: Felix Wiegand Date: Sun, 5 Nov 2023 23:46:17 +0100 Subject: [PATCH] Support android version Ref #4 --- Cargo.toml | 2 +- src/data_source.rs | 2 +- src/data_source/serial.rs | 41 ++++++++-- src/gui.rs | 154 +++++++++++++++++++------------------- src/gui/map.rs | 7 ++ src/gui/tabs/configure.rs | 4 +- src/lib.rs | 6 +- src/settings.rs | 12 +++ src/simulation.rs | 4 +- 9 files changed, 141 insertions(+), 91 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 95f65ad..c328f4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ rand = { version = "0.8", default-features = false, features = ["std", "std_rng" [target.'cfg(target_arch = "aarch64")'.dependencies] home = "0.5" # No home directories in web assembly indicatif = "0.17" -rand = { version = "0.8", default-features = false, features = ["small_rng", "getrandom"] } +rand = { version = "0.8", default-features = false, features = ["std", "std_rng"] } # Web Assembly dependencies [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/src/data_source.rs b/src/data_source.rs index 2a76cdd..7197ef2 100644 --- a/src/data_source.rs +++ b/src/data_source.rs @@ -20,7 +20,7 @@ pub mod serial; pub mod simulation; pub use log_file::LogFileDataSource; -pub use serial::SerialDataSource; +pub use serial::*; pub use simulation::SimulationDataSource; /// Trait shared by all data sources. diff --git a/src/data_source/serial.rs b/src/data_source/serial.rs index bde5d37..57a34af 100644 --- a/src/data_source/serial.rs +++ b/src/data_source/serial.rs @@ -28,6 +28,15 @@ pub const BAUD_RATE: u32 = 115_200; pub const MESSAGE_TIMEOUT: Duration = Duration::from_millis(500); pub const HEARTBEAT_INTERVAL: Duration = Duration::from_millis(500); +// For Android, the Java wrapper has to handle the actual serial port and +// we use these questionable methods to pass the data in via JNI +#[cfg(target_os="android")] +pub static mut DOWNLINK_MESSAGE_RECEIVER: Option> = None; +#[cfg(target_os="android")] +pub static mut UPLINK_MESSAGE_SENDER: Option> = None; +#[cfg(target_os="android")] +pub static mut SERIAL_STATUS_RECEIVER: Option> = None; + /// The current state of our downlink monitoring thread #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SerialStatus { @@ -130,7 +139,7 @@ pub fn find_serial_port() -> Option { /// Continuously monitors for connected USB serial devices and connects to them. /// Run in a separate thread using `spawn_downlink_monitor`. #[cfg(not(target_arch = "wasm32"))] // TODO: serial ports on wasm? -fn downlink_monitor( +pub fn downlink_monitor( serial_status_tx: Sender<(SerialStatus, Option)>, mut downlink_tx: Sender, mut uplink_rx: Receiver, @@ -255,16 +264,31 @@ impl DataSource for SerialDataSource { fn update(&mut self, _ctx: &egui::Context) { self.message_receipt_times.retain(|(i, _)| i.elapsed() < Duration::from_millis(1000)); - for (status, port) in self.serial_status_rx.try_iter() { + #[cfg(target_os="android")] + for status in unsafe { SERIAL_STATUS_RECEIVER.as_mut().unwrap().try_iter() } { + self.serial_status = status; + self.serial_port = Some("".to_owned()); + + if self.serial_status == SerialStatus::Connected { + self.send(UplinkMessage::ApplyLoRaSettings(self.lora_settings.clone())).unwrap(); + } + } + + #[cfg(not(target_os="android"))] + for (status, port) in self.serial_status_rx.try_iter().collect::>().into_iter() { self.serial_status = status; self.serial_port = port; if self.serial_status == SerialStatus::Connected { - self.uplink_tx.send(UplinkMessage::ApplyLoRaSettings(self.lora_settings.clone())).unwrap(); + self.send(UplinkMessage::ApplyLoRaSettings(self.lora_settings.clone())).unwrap(); } } + #[cfg(not(target_os = "android"))] let msgs: Vec<_> = self.downlink_rx.try_iter().collect(); + #[cfg(target_os = "android")] + let msgs: Vec<_> = unsafe { DOWNLINK_MESSAGE_RECEIVER.as_mut().unwrap().try_iter().collect() }; + for msg in msgs.into_iter() { self.write_to_telemetry_log(&msg); @@ -309,11 +333,18 @@ impl DataSource for SerialDataSource { self.message_receipt_times.truncate(0); } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(any(target_arch = "wasm32", target_os="android")))] fn send(&mut self, msg: UplinkMessage) -> Result<(), SendError> { self.uplink_tx.send(msg) } + #[cfg(target_os="android")] + fn send(&mut self, msg: UplinkMessage) -> Result<(), SendError> { + unsafe { + UPLINK_MESSAGE_SENDER.as_mut().unwrap().send(msg) + } + } + #[cfg(target_arch = "wasm32")] fn send(&mut self, msg: UplinkMessage) -> Result<(), SendError> { Ok(()) @@ -345,7 +376,7 @@ impl DataSource for SerialDataSource { fn apply_settings(&mut self, settings: &AppSettings) { self.lora_settings = settings.lora.clone(); - self.uplink_tx.send(UplinkMessage::ApplyLoRaSettings(self.lora_settings.clone())).unwrap(); + self.send(UplinkMessage::ApplyLoRaSettings(self.lora_settings.clone())).unwrap(); } fn link_quality(&self) -> Option { diff --git a/src/gui.rs b/src/gui.rs index 5597a40..9fee05d 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -64,6 +64,7 @@ pub const ARCHIVE: [(&str, Option<&'static str>, Option<&'static str>); 5] = [ ), ]; +#[derive(Debug)] enum ArchiveLoadProgress { Progress((u64, u64)), Complete(Vec), @@ -88,8 +89,7 @@ pub struct Sam { impl Sam { /// Initialize the application, including the state objects for widgets /// such as plots and maps. - pub fn init(cc: &eframe::CreationContext<'_>, settings: AppSettings, data_source: Box) -> Self { - let ctx = &cc.egui_ctx; + pub fn init(ctx: &egui::Context, settings: AppSettings, data_source: Box) -> Self { let mut fonts = egui::FontDefinitions::default(); let roboto = egui::FontData::from_static(include_bytes!("../assets/fonts/RobotoMono-Regular.ttf")); let lato = egui::FontData::from_static(include_bytes!("../assets/fonts/Overpass-Light.ttf")); @@ -137,13 +137,13 @@ impl Sam { let response = match reqwest::Client::new().get(url).send().await { Ok(res) => res, Err(e) => { - let _ = progress_sender.send(ArchiveLoadProgress::Error(e)); + progress_sender.send(ArchiveLoadProgress::Error(e)).unwrap(); return; } }; let total_size = response.content_length().unwrap_or(0); - let _ = progress_sender.send(ArchiveLoadProgress::Progress((0, total_size))); + progress_sender.send(ArchiveLoadProgress::Progress((0, total_size))).unwrap(); let mut cursor = std::io::Cursor::new(Vec::with_capacity(total_size as usize)); let (mut progress, mut last_progress) = (0, 0); @@ -159,13 +159,13 @@ impl Sam { } } Err(e) => { - let _ = progress_sender.send(ArchiveLoadProgress::Error(e)); + progress_sender.send(ArchiveLoadProgress::Error(e)).unwrap(); return; } } } - let _ = progress_sender.send(ArchiveLoadProgress::Complete(cursor.into_inner())); + progress_sender.send(ArchiveLoadProgress::Complete(cursor.into_inner())).unwrap(); let duration = start.elapsed().as_secs_f32(); let mib = (total_size as f32) / 1024.0 / 1024.0; info!("Downloaded {}MiB in {:.1}ms ({}MiB/s)", mib, duration * 1000.0, mib / duration); @@ -310,6 +310,76 @@ impl Sam { } pub fn ui(&mut self, ctx: &egui::Context) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + #[cfg(feature = "profiling")] + puffin::GlobalProfiler::lock().new_frame(); + #[cfg(feature = "profiling")] + puffin_egui::profiler_window(ctx); + + self.data_source.update(ctx); + + // TODO: only send this if we know it's not a ground station? + if self.data_source.fc_settings().is_none() && self.data_source.vehicle_states().next().is_some() { + self.data_source.send(UplinkMessage::ReadSettings).unwrap(); + } + + if let Some(receiver) = self.archive_progress_receiver.as_ref() { + match receiver.try_recv() { + Ok(ArchiveLoadProgress::Progress(progress)) => { + self.archive_progress = Some(progress); + } + Ok(ArchiveLoadProgress::Complete(bytes)) => { + self.open_log_file(LogFileDataSource::from_bytes( + Some("".to_string()), // TODO: show title + bytes, + self.replay_logs, + )); + self.archive_window_open = false; + self.archive_progress_receiver = None; + self.archive_progress = None; + } + Ok(ArchiveLoadProgress::Error(e)) => { + error!("{:?}", e); // TODO: show this visually + self.archive_progress_receiver = None; + self.archive_progress = None; + } + _ => {} + } + } + + // Check for keyboard inputs for tab and flight mode changes + if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::F1)) { + self.tab = GuiTab::Launch; + } else if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::F2)) { + self.tab = GuiTab::Plot; + } else if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::F3)) { + self.tab = GuiTab::Configure; + } + + let shortcut_mode = if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F5)) { + Some(FlightMode::Idle) + } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F6)) { + Some(FlightMode::HardwareArmed) + } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F7)) { + Some(FlightMode::Armed) + } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F8)) { + Some(FlightMode::Flight) + } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F9)) { + Some(FlightMode::RecoveryDrogue) + } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F10)) { + Some(FlightMode::RecoveryMain) + } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F11)) { + Some(FlightMode::Landed) + } else { + None + }; + + // Set new flight mode if keyboard shortcut was used + if let Some(fm) = shortcut_mode { + self.data_source.send_command(Command::SetFlightMode(fm)).unwrap(); + } + // Redefine text_styles let colors = ThemeColors::new(ctx); let mut style = (*ctx.style()).clone(); @@ -526,76 +596,6 @@ impl Sam { impl eframe::App for Sam { /// Main draw method of the application fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - #[cfg(feature = "profiling")] - puffin::profile_function!(); - #[cfg(feature = "profiling")] - puffin::GlobalProfiler::lock().new_frame(); - #[cfg(feature = "profiling")] - puffin_egui::profiler_window(ctx); - - self.data_source.update(ctx); - - // TODO: only send this if we know it's not a ground station? - if self.data_source.fc_settings().is_none() && self.data_source.vehicle_states().next().is_some() { - self.data_source.send(UplinkMessage::ReadSettings).unwrap(); - } - - if let Some(receiver) = self.archive_progress_receiver.as_ref() { - match receiver.try_recv() { - Ok(ArchiveLoadProgress::Progress(progress)) => { - self.archive_progress = Some(progress); - } - Ok(ArchiveLoadProgress::Complete(bytes)) => { - self.open_log_file(LogFileDataSource::from_bytes( - Some("".to_string()), // TODO: show title - bytes, - self.replay_logs, - )); - self.archive_window_open = false; - self.archive_progress_receiver = None; - self.archive_progress = None; - } - Ok(ArchiveLoadProgress::Error(e)) => { - error!("{:?}", e); // TODO: show this visually - self.archive_progress_receiver = None; - self.archive_progress = None; - } - _ => {} - } - } - - // Check for keyboard inputs for tab and flight mode changes - if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::F1)) { - self.tab = GuiTab::Launch; - } else if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::F2)) { - self.tab = GuiTab::Plot; - } else if ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::F3)) { - self.tab = GuiTab::Configure; - } - - let shortcut_mode = if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F5)) { - Some(FlightMode::Idle) - } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F6)) { - Some(FlightMode::HardwareArmed) - } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F7)) { - Some(FlightMode::Armed) - } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F8)) { - Some(FlightMode::Flight) - } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F9)) { - Some(FlightMode::RecoveryDrogue) - } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F10)) { - Some(FlightMode::RecoveryMain) - } else if ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F11)) { - Some(FlightMode::Landed) - } else { - None - }; - - // Set new flight mode if keyboard shortcut was used - if let Some(fm) = shortcut_mode { - self.data_source.send_command(Command::SetFlightMode(fm)).unwrap(); - } - // Draw UI self.ui(ctx) } @@ -620,7 +620,7 @@ pub fn main(log_file: Option) -> Result<(), Box> initial_window_size: Some(egui::vec2(1000.0, 700.0)), ..Default::default() }, - Box::new(|cc| Box::new(Sam::init(cc, app_settings, data_source))), + Box::new(|cc| Box::new(Sam::init(&cc.egui_ctx, app_settings, data_source))), )?; Ok(()) diff --git a/src/gui/map.rs b/src/gui/map.rs index 8ea4917..35e61a0 100644 --- a/src/gui/map.rs +++ b/src/gui/map.rs @@ -39,8 +39,15 @@ fn tile_id(tile: &Tile) -> String { #[cfg(not(target_arch = "wasm32"))] fn load_tile_bytes(tile: &Tile, access_token: &String) -> Result, Box> { + #[cfg(not(target_os="android"))] let project_dirs = directories::ProjectDirs::from("space", "tudsat", "sam").unwrap(); + #[cfg(not(target_os="android"))] let cache_dir = project_dirs.cache_dir(); + + // TODO: avoid hardcoding this + #[cfg(target_os="android")] + let cache_dir = std::path::Path::new("/data/user/0/space.tudsat.sam/cache"); + if !cache_dir.exists() { std::fs::create_dir_all(cache_dir)?; } diff --git a/src/gui/tabs/configure.rs b/src/gui/tabs/configure.rs index 8cf6f86..1a7ad2d 100644 --- a/src/gui/tabs/configure.rs +++ b/src/gui/tabs/configure.rs @@ -185,12 +185,12 @@ impl ConfigureTab { data_source.send(UplinkMessage::WriteSettings(settings)).unwrap(); } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(any(target_arch = "wasm32", target_arch="aarch64")))] if ui.add_enabled(data_source.fc_settings().is_some(), Button::new("🖹 Save to File")).clicked() { save_fc_settings_file(&data_source.fc_settings().unwrap()); } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(any(target_arch = "wasm32", target_arch="aarch64")))] if ui.add_enabled(data_source.fc_settings().is_some(), Button::new("🖹 Load from File")).clicked() { if let Some(settings) = open_fc_settings_file() { diff --git a/src/lib.rs b/src/lib.rs index 4ff902b..99ee8ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,13 +5,13 @@ // the application to silence warnings. #[allow(dead_code)] #[allow(unused_variables)] -mod data_source; +pub mod data_source; #[allow(unused_imports)] mod file; #[allow(dead_code)] #[allow(unused_imports)] mod gui; -mod settings; +pub mod settings; mod simulation; mod state; mod telemetry_ext; @@ -60,7 +60,7 @@ impl WebHandle { .start( canvas_id, eframe::WebOptions::default(), - Box::new(|cc| Box::new(Sam::init(cc, AppSettings::default(), data_source))), + Box::new(|cc| Box::new(Sam::init(&cc.egui_ctx, AppSettings::default(), data_source))), ) .await } diff --git a/src/settings.rs b/src/settings.rs index 07f2e27..92c8bff 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -12,8 +12,14 @@ pub struct AppSettings { impl AppSettings { pub fn load() -> Result> { + #[cfg(not(target_os="android"))] let project_dirs = directories::ProjectDirs::from("space", "tudsat", "sam").unwrap(); + #[cfg(not(target_os="android"))] let config_dir = project_dirs.config_dir(); + + #[cfg(target_os="android")] + let config_dir = std::path::Path::new("/data/user/0/space.tudsat.sam/files"); + if !config_dir.exists() { std::fs::create_dir_all(config_dir)?; } @@ -24,8 +30,14 @@ impl AppSettings { } pub fn save(&self) -> Result<(), Box> { + #[cfg(not(target_os="android"))] let project_dirs = directories::ProjectDirs::from("space", "tudsat", "sam").unwrap(); + #[cfg(not(target_os="android"))] let config_dir = project_dirs.config_dir(); + + #[cfg(target_os="android")] + let config_dir = std::path::Path::new("/data/user/0/space.tudsat.sam/files"); + if !config_dir.exists() { std::fs::create_dir_all(config_dir)?; } diff --git a/src/simulation.rs b/src/simulation.rs index df4ef47..28adffc 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -13,9 +13,9 @@ use crate::state::VehicleState; use crate::gui::ARCHIVE; -#[cfg(target_os="windows")] +#[cfg(any(target_os="windows", target_arch="aarch64"))] type Rng = rand::rngs::StdRng; -#[cfg(not(target_os="windows"))] +#[cfg(not(any(target_os="windows", target_arch="aarch64")))] type Rng = rand::rngs::SmallRng; const GRAVITY: f32 = 9.80665;