diff --git a/Cargo.lock b/Cargo.lock index 63cc8de..61f5f52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -504,12 +504,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.154" @@ -636,7 +630,6 @@ dependencies = [ "fern", "homedir", "humantime", - "lazy_static", "log", "mio", "num_enum", @@ -776,7 +769,7 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "satrs" version = "0.2.0-rc.5" -source = "git+https://egit.irs.uni-stuttgart.de/rust/sat-rs.git?branch=main#29f71c2a571e7492cf5d997d4c11c3f844de83bc" +source = "git+https://egit.irs.uni-stuttgart.de/rust/sat-rs.git?branch=main#424dfc439c52223c8480e67413c8305dad5a75dc" dependencies = [ "bus", "cobs", @@ -835,9 +828,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.199" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] @@ -855,9 +848,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.199" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3f999f1..9d68074 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ fern = "0.6" toml = "0.8" chrono = "0.4" log = "0.4" -lazy_static = "1" humantime = "2" strum = { version = "0.26", features = ["derive"] } thiserror = "1" diff --git a/src/config.rs b/src/config.rs index 180f131..323871e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,3 @@ -use lazy_static::lazy_static; use num_enum::{IntoPrimitive, TryFromPrimitive}; use once_cell::sync::OnceCell; use satrs::events::{EventU32TypedSev, SeverityInfo}; @@ -29,8 +28,11 @@ pub const VALID_PACKET_ID_LIST: &[PacketId] = &[PacketId::new_for_tc(true, EXPER pub const SPP_CLIENT_WIRETAPPING_RX: bool = false; pub const SPP_CLIENT_WIRETAPPING_TX: bool = false; +pub const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); + pub static TO_GROUND_FOLDER_DIR: OnceCell = OnceCell::new(); pub static TO_GROUND_LP_FOLDER_DIR: OnceCell = OnceCell::new(); +pub static HOME_PATH: OnceCell = OnceCell::new(); #[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)] #[repr(u8)] @@ -53,31 +55,32 @@ pub enum GroupId { pub const TEST_EVENT: EventU32TypedSev = EventU32TypedSev::::new(GroupId::Tmtc as u16, 0); -pub const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); +pub fn set_up_home_path() { + let mut home_path = PathBuf::new(); + let home_path_default = homedir::get_my_home() + .expect("Getting home dir from OS failed.") + .expect("No home dir found."); -lazy_static! { - pub static ref HOME_PATH: PathBuf = { - let mut home_path = PathBuf::new(); - let home_path_default = homedir::get_my_home() - .expect("Getting home dir from OS failed.") - .expect("No home dir found."); - - home_path.push(if Path::new(HOME_FOLDER_EXPERIMENT).exists() { - HOME_FOLDER_EXPERIMENT - } else { - home_path_default - .to_str() - .expect("Error converting to string.") - }); - home_path - }; + home_path.push(if Path::new(HOME_FOLDER_EXPERIMENT).exists() { + HOME_FOLDER_EXPERIMENT + } else { + home_path_default + .to_str() + .expect("Error converting to string.") + }); + HOME_PATH + .set(home_path) + .expect("attempting to set once cell twice") } -pub fn set_up_low_prio_ground_dir() { +pub fn set_up_low_prio_ground_dir(base_path: PathBuf) { + /* #[cfg(feature = "host")] let mut to_ground_lp_dir = std::env::current_dir().expect("getting current dir failed"); #[cfg(not(feature = "host"))] - let mut to_ground_lp_dir = HOME_PATH.clone(); + let mut to_ground_lp_dir = home_path; + */ + let mut to_ground_lp_dir = base_path.to_path_buf(); to_ground_lp_dir.push(TO_GROUND_LP_FOLDER_NAME); if !Path::new(&to_ground_lp_dir).exists() { log::info!( @@ -96,11 +99,14 @@ pub fn set_up_low_prio_ground_dir() { .expect("attemting to set once cell twice"); } -pub fn set_up_ground_dir() { +pub fn set_up_ground_dir(base_path: PathBuf) { + /* #[cfg(feature = "host")] let mut to_ground_dir = std::env::current_dir().expect("getting current dir failed"); #[cfg(not(feature = "host"))] let mut to_ground_dir = HOME_PATH.clone(); + */ + let mut to_ground_dir = base_path.to_path_buf(); to_ground_dir.push(TO_GROUND_FOLDER_NAME); if !Path::new(&to_ground_dir).exists() { log::info!("creating to ground directory at {:?}", to_ground_dir); @@ -123,7 +129,7 @@ pub mod cfg_file { path::{Path, PathBuf}, }; - use super::{CONFIG_FILE_NAME, HOME_PATH, TCP_SPP_SERVER_PORT}; + use super::{CONFIG_FILE_NAME, TCP_SPP_SERVER_PORT}; pub const SPP_CLIENT_PORT_CFG_KEY: &str = "tcp_spp_server_port"; @@ -140,8 +146,8 @@ pub mod cfg_file { } } - pub fn create_app_config() -> AppCfg { - let mut cfg_path = HOME_PATH.clone(); + pub fn create_app_config(base_path: PathBuf) -> AppCfg { + let mut cfg_path = base_path; cfg_path.push(CONFIG_FILE_NAME); let cfg_path_home = cfg_path.as_path(); let relevant_path = if Path::new(CONFIG_FILE_NAME).exists() { diff --git a/src/controller.rs b/src/controller.rs index 92bf41e..16408aa 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -47,6 +47,7 @@ pub enum ActionId { #[derive(Debug)] pub struct ControllerPathCollection { + pub home_path: PathBuf, pub stop_file_home_path: PathBuf, pub stop_file_tmp_path: PathBuf, pub to_ground_dir: PathBuf, @@ -55,12 +56,14 @@ pub struct ControllerPathCollection { impl Default for ControllerPathCollection { fn default() -> Self { + let home_path = HOME_PATH.get().unwrap(); let mut home_path_stop_file = PathBuf::new(); - home_path_stop_file.push(HOME_PATH.as_path()); + home_path_stop_file.push(home_path); home_path_stop_file.push(STOP_FILE_NAME); let mut tmp_path_stop_file = temp_dir(); tmp_path_stop_file.push(STOP_FILE_NAME); Self { + home_path: home_path.clone(), stop_file_home_path: home_path_stop_file, stop_file_tmp_path: tmp_path_stop_file, to_ground_dir: TO_GROUND_FOLDER_DIR @@ -159,7 +162,7 @@ impl ExperimentController { ) -> io::Result<()> { log::info!("moving images into low priority downlink folder"); let num_moved_files = move_images_inside_home_dir_to_low_prio_ground_dir( - &HOME_PATH, + HOME_PATH.get().unwrap(), &self.paths.to_ground_low_prio_dir, )?; log::info!("moved {} image files", num_moved_files); @@ -405,6 +408,7 @@ mod tests { to_ground_low_prio_dir.push("toGroundLP"); let test_paths = ControllerPathCollection { + home_path: test_tmp_dir.path().to_path_buf(), stop_file_home_path, stop_file_tmp_path, to_ground_dir, diff --git a/src/handlers/camera.rs b/src/handlers/camera.rs index 0c3c3c9..6788131 100644 --- a/src/handlers/camera.rs +++ b/src/handlers/camera.rs @@ -30,7 +30,7 @@ use derive_new::new; use log::info; use num_enum::TryFromPrimitive; use ops_sat_rs::config::cam_error::{self, CameraError}; -use ops_sat_rs::config::GENERIC_FAILED; +use ops_sat_rs::config::{GENERIC_FAILED, HOME_PATH}; use ops_sat_rs::TimeStampHelper; use satrs::action::{ActionRequest, ActionRequestVariant}; use satrs::hk::HkRequest; @@ -40,8 +40,10 @@ use satrs::request::{GenericMessage, MessageMetadata, UniqueApidTargetId}; use satrs::res_code::ResultU16; use satrs::tmtc::PacketAsVec; use serde::{Deserialize, Serialize}; +use std::io::{self, Write}; use std::process::{Command, Output}; use std::sync::mpsc; +use std::time::{SystemTime, UNIX_EPOCH}; const IMS_TESTAPP: &str = "ims100_testapp"; @@ -101,7 +103,7 @@ pub enum ActionId { // TODO what happens if limits are exceded #[allow(non_snake_case)] -#[derive(Debug, Serialize, Deserialize, new)] +#[derive(Debug, Clone, Serialize, Deserialize, new)] pub struct CameraPictureParameters { pub R: u8, pub G: u8, @@ -112,19 +114,61 @@ pub struct CameraPictureParameters { pub W: u32, // wait time between pictures in ms, max: 40000 } -#[derive(Debug)] -pub struct Ims100BatchHandler { +pub trait TakeImageExecutor { + fn take_image(&self, param: &CameraPictureParameters) -> io::Result<(Command, Output)>; +} + +#[derive(Default)] +pub struct Ims100ImageExecutor {} + +pub fn build_take_image_command(param: &CameraPictureParameters) -> Command { + let mut cmd = Command::new(IMS_TESTAPP); + cmd.arg("-R") + .arg(param.R.to_string()) + .arg("-G") + .arg(param.G.to_string()) + .arg("-B") + .arg(param.B.to_string()) + .arg("-c") + .arg("/dev/cam_tty") + .arg("-m") + .arg("/dev/cam_sd") + .arg("-v") + .arg("0") + .arg("-n") + .arg(param.N.to_string()); + if param.P { + cmd.arg("-p"); + } + cmd.arg("-e") + .arg(param.E.to_string()) + .arg("-w") + .arg(param.W.to_string()); + cmd +} + +impl TakeImageExecutor for Ims100ImageExecutor { + fn take_image(&self, param: &CameraPictureParameters) -> io::Result<(Command, Output)> { + let mut cmd = build_take_image_command(param); + info!("taking image with command: {cmd:?}"); + let output = cmd.output()?; + Ok((cmd, output)) + } +} + +pub struct Ims100BatchHandler { id: UniqueApidTargetId, + image_executor: ImgExecutor, composite_request_rx: mpsc::Receiver>, tm_tx: mpsc::Sender, action_reply_tx: mpsc::Sender>, stamp_helper: TimeStampHelper, } -#[allow(non_snake_case)] -impl Ims100BatchHandler { +impl Ims100BatchHandler { pub fn new( id: UniqueApidTargetId, + image_executor: ImgExecutor, composite_request_rx: mpsc::Receiver>, tm_tx: mpsc::Sender, action_reply_tx: mpsc::Sender>, @@ -132,6 +176,7 @@ impl Ims100BatchHandler { ) -> Self { Self { id, + image_executor, composite_request_rx, tm_tx, action_reply_tx, @@ -223,14 +268,17 @@ impl Ims100BatchHandler { }, }; match self.take_picture(¶m) { - Ok(ref output) => { + Ok((cmd, ref output)) => { self.send_completion_success(requestor_info, action_request); if let Err(e) = send_data_reply(self.id, &output.stdout, &self.stamp_helper, &self.tm_tx) { log::error!("sending data reply unexpectedly failed: {e}"); } - self.create_metadata_file(¶m); + if let Err(e) = self.create_metadata_file(cmd, ¶m) { + // TODO: Generate event? + log::error!("issue creating metadata file: {e}"); + } } Err(e) => match e { CameraError::TakeImageError(ref err_str) => { @@ -262,8 +310,25 @@ impl Ims100BatchHandler { } } - pub fn create_metadata_file(&mut self, _param: &CameraPictureParameters) { - // TODO: Implement + pub fn create_metadata_file( + &mut self, + cmd: Command, + param: &CameraPictureParameters, + ) -> io::Result<()> { + let now = SystemTime::now(); + let unix_timestamp = now.duration_since(UNIX_EPOCH); + if unix_timestamp.is_err() { + log::error!("failed to get unix timestamp, time went backwards?"); + return Ok(()); + } + let unix_timestamp = unix_timestamp.unwrap().as_millis(); + let mut metadata_path = HOME_PATH.get().unwrap().clone(); + metadata_path.push(format!("img_msec_{}.txt", unix_timestamp)); + let mut file = std::fs::File::create(metadata_path)?; + writeln!(file, "time: {}", humantime::format_rfc3339_seconds(now))?; + writeln!(file, "cmd params: {:?}", param)?; + writeln!(file, "cmd: {:?}", cmd)?; + Ok(()) } pub fn send_completion_success(&self, requestor: &MessageMetadata, action_req: &ActionRequest) { @@ -294,31 +359,11 @@ impl Ims100BatchHandler { } } - pub fn take_picture(&mut self, param: &CameraPictureParameters) -> Result { - let mut cmd = Command::new(IMS_TESTAPP); - cmd.arg("-R") - .arg(param.R.to_string()) - .arg("-G") - .arg(param.G.to_string()) - .arg("-B") - .arg(param.B.to_string()) - .arg("-c") - .arg("/dev/cam_tty") - .arg("-m") - .arg("/dev/cam_sd") - .arg("-v") - .arg("0") - .arg("-n") - .arg(param.N.to_string()); - if param.P { - cmd.arg("-p"); - } - cmd.arg("-e") - .arg(param.E.to_string()) - .arg("-w") - .arg(param.W.to_string()); - info!("taking image with command: {cmd:?}"); - let output = cmd.output()?; + pub fn take_picture( + &mut self, + param: &CameraPictureParameters, + ) -> Result<(Command, Output), CameraError> { + let (cmd, output) = self.image_executor.take_image(param)?; info!("imager cmd status: {}", &output.status); info!("imager output: {}", String::from_utf8_lossy(&output.stdout)); @@ -330,7 +375,7 @@ impl Ims100BatchHandler { if !output.status.success() { return Err(CameraError::TakeImageError(error_string.to_string())); } - Ok(output) + Ok((cmd, output)) } #[allow(dead_code)] @@ -349,6 +394,25 @@ impl Ims100BatchHandler { } } +impl Ims100BatchHandler { + pub fn new_with_default_img_executor( + id: UniqueApidTargetId, + composite_request_rx: mpsc::Receiver>, + tm_tx: mpsc::Sender, + action_reply_tx: mpsc::Sender>, + stamp_helper: TimeStampHelper, + ) -> Self { + Self::new( + id, + Ims100ImageExecutor::default(), + composite_request_rx, + tm_tx, + action_reply_tx, + stamp_helper, + ) + } +} + #[cfg(test)] mod tests { use crate::handlers::camera::{ @@ -356,35 +420,76 @@ mod tests { }; use crate::requests::CompositeRequest; use ops_sat_rs::config::components::CAMERA_HANDLER; + use ops_sat_rs::config::HOME_PATH; use ops_sat_rs::TimeStampHelper; use satrs::action::{ActionRequest, ActionRequestVariant}; - use satrs::pus::action::ActionReplyPus; + use satrs::pus::action::{ActionReplyPus, ActionReplyVariant}; use satrs::request::{GenericMessage, MessageMetadata}; use satrs::tmtc::PacketAsVec; + use satrs::ComponentId; + use std::cell::RefCell; + use std::collections::VecDeque; + use std::os::unix::process::ExitStatusExt; use std::sync::mpsc; + use tempfile::tempdir; + + use super::{build_take_image_command, TakeImageExecutor}; + + const REQUESTOR_ID: ComponentId = 1; + + #[derive(Default)] + struct Ims100TestImageExecutor { + pub called_with_params: RefCell>, + } + + impl TakeImageExecutor for Ims100TestImageExecutor { + fn take_image( + &self, + param: &CameraPictureParameters, + ) -> std::io::Result<(std::process::Command, std::process::Output)> { + let mut param_deque = self.called_with_params.borrow_mut(); + param_deque.push_back(param.clone()); + // We fake the test output, with no way to execute the actual command. + let output = std::process::Output { + status: std::process::ExitStatus::from_raw(0), + stdout: Vec::new(), + stderr: Vec::new(), + }; + // We could generate the files as they are generated by the real batch handler.. But + // I think it's okay to verify that the function is called with the correct parameters + // and the metadata file is created for now. + Ok((build_take_image_command(param), output)) + } + } #[allow(dead_code)] - struct Ims1000Testbench { - pub handler: Ims100BatchHandler, + struct Ims100Testbench { + pub handler: Ims100BatchHandler, pub composite_req_tx: mpsc::Sender>, pub tm_receiver: mpsc::Receiver, pub action_reply_rx: mpsc::Receiver>, } - impl Default for Ims1000Testbench { + impl Default for Ims100Testbench { fn default() -> Self { + // TODO: Set home path which is used inside the batch handler for creating metadata. + let temp_dir = tempdir().expect("errror creating temp directory"); + HOME_PATH + .set(temp_dir.path().to_path_buf()) + .expect("error setting test home path"); let (composite_request_tx, composite_request_rx) = mpsc::channel(); let (tm_tx, tm_rx) = mpsc::channel(); let (action_reply_tx, action_reply_rx) = mpsc::channel(); let time_helper = TimeStampHelper::default(); - let cam_handler: Ims100BatchHandler = Ims100BatchHandler::new( + let cam_handler = Ims100BatchHandler::new( CAMERA_HANDLER, + Ims100TestImageExecutor::default(), composite_request_rx, tm_tx, action_reply_tx, time_helper, ); - Ims1000Testbench { + Ims100Testbench { handler: cam_handler, composite_req_tx: composite_request_tx, tm_receiver: tm_rx, @@ -395,7 +500,7 @@ mod tests { #[test] fn command_line_execution() { - let mut testbench = Ims1000Testbench::default(); + let mut testbench = Ims100Testbench::default(); testbench .handler .take_picture(&DEFAULT_SINGLE_FLATSAT_CAM_PARAMS) @@ -411,8 +516,9 @@ mod tests { } #[test] - fn test_action_req() { - let mut testbench = Ims1000Testbench::default(); + fn test_take_image_action_req() { + let request_id = 5; + let mut testbench = Ims100Testbench::default(); let data = serde_json::to_string(&DEFAULT_SINGLE_FLATSAT_CAM_PARAMS).unwrap(); let req = ActionRequest::new( ActionId::CustomParameters as u32, @@ -421,12 +527,23 @@ mod tests { testbench .handler - .handle_action_request(&MessageMetadata::new(1, 1), &req) + .handle_action_request(&MessageMetadata::new(request_id, REQUESTOR_ID), &req); + // TODO: Verify execution and generated metadata file. + let action_reply = testbench + .action_reply_rx + .try_recv() + .expect("expected action reply"); + assert!(matches!( + action_reply.message.variant, + ActionReplyVariant::Completed + )); + assert_eq!(action_reply.request_id(), request_id); + assert_eq!(action_reply.sender_id(), REQUESTOR_ID); } #[test] fn test_action_req_channel() { - let mut testbench = Ims1000Testbench::default(); + let mut testbench = Ims100Testbench::default(); let data = serde_json::to_string(&DEFAULT_SINGLE_FLATSAT_CAM_PARAMS).unwrap(); let req = ActionRequest::new( diff --git a/src/main.rs b/src/main.rs index 7e84a46..585c7ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,21 @@ use std::{ env::temp_dir, net::{IpAddr, SocketAddr}, - path::PathBuf, sync::{atomic::AtomicBool, mpsc, Arc}, thread, time::Duration, }; use log::info; +#[cfg(not(feature = "host"))] +use ops_sat_rs::config::HOME_PATH; use ops_sat_rs::config::{ cfg_file::create_app_config, components::{CONTROLLER_ID, TCP_SERVER, TCP_SPP_CLIENT, UDP_SERVER}, pool::create_sched_tc_pool, - set_up_ground_dir, set_up_low_prio_ground_dir, + set_up_ground_dir, set_up_home_path, set_up_low_prio_ground_dir, tasks::{FREQ_MS_CAMERA_HANDLING, FREQ_MS_CTRL, FREQ_MS_PUS_STACK, STOP_CHECK_FREQUENCY}, - HOME_PATH, STOP_FILE_NAME, VALID_PACKET_ID_LIST, VERSION, + STOP_FILE_NAME, VALID_PACKET_ID_LIST, VERSION, }; use ops_sat_rs::config::{components::CAMERA_HANDLER, tasks::FREQ_MS_EVENT_HANDLING}; use ops_sat_rs::config::{tasks::FREQ_MS_UDP_TMTC, OBSW_SERVER_ADDR, SERVER_PORT}; @@ -58,10 +59,18 @@ fn main() { let version_str = VERSION.unwrap_or("?"); println!("OPS-SAT Rust Experiment OBSW v{}", version_str); setup_logger().expect("setting up logging with fern failed"); - set_up_low_prio_ground_dir(); - set_up_ground_dir(); - let app_cfg = create_app_config(); + set_up_home_path(); + #[cfg(feature = "host")] + let base_dir = std::env::current_dir() + .expect("getting current dir failed") + .to_path_buf(); + #[cfg(not(feature = "host"))] + let base_dir = HOME_PATH.get().unwrap(); + set_up_low_prio_ground_dir(base_dir.clone()); + set_up_ground_dir(base_dir.clone()); + + let app_cfg = create_app_config(base_dir.clone()); info!("App Configuration: {:?}", app_cfg); let stop_signal = Arc::new(AtomicBool::new(false)); @@ -194,8 +203,7 @@ fn main() { stop_signal.clone(), ); - let mut home_path_stop_file = PathBuf::new(); - home_path_stop_file.push(HOME_PATH.as_path()); + let mut home_path_stop_file = base_dir.clone(); home_path_stop_file.push(STOP_FILE_NAME); let mut tmp_path_stop_file = temp_dir(); tmp_path_stop_file.push(STOP_FILE_NAME); @@ -217,7 +225,7 @@ fn main() { .expect("creating TCP SPP client failed"); let timestamp_helper = TimeStampHelper::default(); - let mut camera_handler: Ims100BatchHandler = Ims100BatchHandler::new( + let mut camera_handler: Ims100BatchHandler = Ims100BatchHandler::new_with_default_img_executor( CAMERA_HANDLER, camera_composite_rx, tm_funnel_tx.clone(),