take image metadata generation #30

Merged
muellerr merged 3 commits from take-img-metadata-impl into main 2024-05-02 13:15:17 +02:00
6 changed files with 270 additions and 88 deletions

7
Cargo.lock generated
View File

@ -515,12 +515,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"
@ -648,7 +642,6 @@ dependencies = [
"fern",
"homedir",
"humantime",
"lazy_static",
"log",
"mio",
"num_enum",

View File

@ -11,7 +11,6 @@ toml = "0.8"
chrono = "0.4"
log = "0.4"
delegate = "0.12"
lazy_static = "1"
humantime = "2"
strum = { version = "0.26", features = ["derive"] }
thiserror = "1"

View File

@ -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<PathBuf> = OnceCell::new();
pub static TO_GROUND_LP_FOLDER_DIR: OnceCell<PathBuf> = OnceCell::new();
pub static HOME_PATH: OnceCell<PathBuf> = OnceCell::new();
#[derive(Copy, Clone, PartialEq, Eq, Debug, TryFromPrimitive, IntoPrimitive)]
#[repr(u8)]
@ -53,10 +55,7 @@ pub enum GroupId {
pub const TEST_EVENT: EventU32TypedSev<SeverityInfo> =
EventU32TypedSev::<SeverityInfo>::new(GroupId::Tmtc as u16, 0);
pub const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
lazy_static! {
pub static ref HOME_PATH: PathBuf = {
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.")
@ -69,15 +68,19 @@ lazy_static! {
.to_str()
.expect("Error converting to string.")
});
home_path
};
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() {

View File

@ -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,

View File

@ -40,8 +40,11 @@ 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::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::mpsc;
use std::time::{SystemTime, UNIX_EPOCH};
const IMS_TESTAPP: &str = "ims100_testapp";
@ -101,7 +104,7 @@ pub enum ActionId {
// TODO what happens if limits are exceded
#[allow(non_snake_case)]
#[derive(Debug, Serialize, Deserialize, new)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, new)]
pub struct CameraPictureParameters {
pub R: u8,
pub G: u8,
@ -112,19 +115,63 @@ 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<ImgExecutor: TakeImageExecutor = Ims100ImageExecutor> {
id: UniqueApidTargetId,
pub image_executor: ImgExecutor,
pub home_path: PathBuf,
composite_request_rx: mpsc::Receiver<GenericMessage<CompositeRequest>>,
tm_tx: mpsc::Sender<PacketAsVec>,
action_reply_tx: mpsc::Sender<GenericMessage<ActionReplyPus>>,
stamp_helper: TimeStampHelper,
}
#[allow(non_snake_case)]
impl Ims100BatchHandler {
impl<ImgExecutor: TakeImageExecutor> Ims100BatchHandler<ImgExecutor> {
pub fn new(
id: UniqueApidTargetId,
image_executor: ImgExecutor,
home_path: &Path,
composite_request_rx: mpsc::Receiver<GenericMessage<CompositeRequest>>,
tm_tx: mpsc::Sender<PacketAsVec>,
action_reply_tx: mpsc::Sender<GenericMessage<ActionReplyPus>>,
@ -132,6 +179,8 @@ impl Ims100BatchHandler {
) -> Self {
Self {
id,
image_executor,
home_path: home_path.to_path_buf(),
composite_request_rx,
tm_tx,
action_reply_tx,
@ -223,14 +272,17 @@ impl Ims100BatchHandler {
},
};
match self.take_picture(&param) {
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(&param);
if let Err(e) = self.create_metadata_file(cmd, &param) {
// TODO: Generate event?
log::error!("issue creating metadata file: {e}");
}
}
Err(e) => match e {
CameraError::TakeImageError(ref err_str) => {
@ -262,8 +314,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 = self.home_path.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 +363,11 @@ impl Ims100BatchHandler {
}
}
pub fn take_picture(&mut self, param: &CameraPictureParameters) -> Result<Output, CameraError> {
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 +379,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 +398,27 @@ impl Ims100BatchHandler {
}
}
impl Ims100BatchHandler {
pub fn new_with_default_img_executor(
id: UniqueApidTargetId,
home_path: &Path,
composite_request_rx: mpsc::Receiver<GenericMessage<CompositeRequest>>,
tm_tx: mpsc::Sender<PacketAsVec>,
action_reply_tx: mpsc::Sender<GenericMessage<ActionReplyPus>>,
stamp_helper: TimeStampHelper,
) -> Self {
Self::new(
id,
Ims100ImageExecutor::default(),
home_path,
composite_request_rx,
tm_tx,
action_reply_tx,
stamp_helper,
)
}
}
#[cfg(test)]
mod tests {
use crate::handlers::camera::{
@ -358,34 +428,77 @@ mod tests {
use ops_sat_rs::config::components::CAMERA_HANDLER;
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::fs::File;
use std::io::{BufRead, BufReader};
use std::os::unix::process::ExitStatusExt;
use std::sync::mpsc;
use tempfile::{tempdir, TempDir};
use super::{build_take_image_command, TakeImageExecutor};
const REQUESTOR_ID: ComponentId = 1;
#[derive(Default)]
struct Ims100TestImageExecutor {
pub called_with_params: RefCell<VecDeque<CameraPictureParameters>>,
}
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<Ims100TestImageExecutor>,
pub tmp_home_dir: TempDir,
pub composite_req_tx: mpsc::Sender<GenericMessage<CompositeRequest>>,
pub tm_receiver: mpsc::Receiver<PacketAsVec>,
pub action_reply_rx: mpsc::Receiver<GenericMessage<ActionReplyPus>>,
}
impl Default for Ims1000Testbench {
impl Default for Ims100Testbench {
fn default() -> Self {
let tmp_home_dir = tempdir().expect("errror creating temp directory");
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(),
tmp_home_dir.path(),
composite_request_rx,
tm_tx,
action_reply_tx,
time_helper,
);
Ims1000Testbench {
// Even though we set the temporary home directory into HOME_PATH, we still need to
// cache the TempDir, so it is not dropped.
Ims100Testbench {
handler: cam_handler,
tmp_home_dir,
composite_req_tx: composite_request_tx,
tm_receiver: tm_rx,
action_reply_rx,
@ -395,7 +508,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 +524,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 +535,71 @@ mod tests {
testbench
.handler
.handle_action_request(&MessageMetadata::new(1, 1), &req)
.handle_action_request(&MessageMetadata::new(request_id, REQUESTOR_ID), &req);
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);
let mut image_executor = testbench
.handler
.image_executor
.called_with_params
.borrow_mut();
let called_params = image_executor.pop_front().expect("expected called params");
assert_eq!(called_params, DEFAULT_SINGLE_FLATSAT_CAM_PARAMS);
let mut detected_metadata_file = false;
for dir_entry_result in std::fs::read_dir(&testbench.handler.home_path)
.unwrap_or_else(|_| panic!("can not read {:?}", testbench.handler.home_path.as_path()))
{
if let Ok(dir_entry) = &dir_entry_result {
if let Ok(file_type) = dir_entry.file_type() {
if file_type.is_file() {
let path_name = dir_entry.file_name();
let path_name_str = path_name.to_string_lossy();
if path_name_str.contains("img_msec_") {
let file = File::open(dir_entry.path()).expect("file not found");
let buf_reader = BufReader::new(file);
for (idx, line) in buf_reader.lines().enumerate() {
let line = line.expect("line is not proper string");
if idx == 0 {
assert!(line.contains("time:"));
// Tricky to check, would have to mock this.. I think it's okay
// for now.
}
if idx == 1 {
assert!(line.contains("cmd params:"));
assert!(line.contains(&format!(
"{:?}",
&DEFAULT_SINGLE_FLATSAT_CAM_PARAMS
)));
}
if idx == 2 {
assert!(line.contains("cmd:"));
let cmd = build_take_image_command(
&DEFAULT_SINGLE_FLATSAT_CAM_PARAMS,
);
let cmd_str = format!("{:?}", cmd);
assert!(line.contains(&cmd_str));
}
}
detected_metadata_file = true;
}
}
}
}
}
assert!(detected_metadata_file, "no metadata file was generated");
}
#[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(

View File

@ -1,7 +1,6 @@
use std::{
env::temp_dir,
net::{IpAddr, SocketAddr},
path::PathBuf,
sync::{atomic::AtomicBool, mpsc, Arc},
thread,
time::Duration,
@ -12,7 +11,7 @@ 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,
};
@ -58,10 +57,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 +201,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,8 +223,9 @@ 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,
HOME_PATH.get().unwrap(),
camera_composite_rx,
tm_funnel_tx.clone(),
pus_action_reply_tx.clone(),