diff --git a/Cargo.lock b/Cargo.lock index 7772ac0..8bfd1c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -489,6 +489,8 @@ dependencies = [ "num_enum", "satrs", "satrs-mib", + "serde", + "serde_json", "strum", "thiserror", ] @@ -680,6 +682,17 @@ dependencies = [ "syn 2.0.59", ] +[[package]] +name = "serde_json" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "smallvec" version = "0.6.14" diff --git a/Cargo.toml b/Cargo.toml index f724140..46ab7bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ strum = { version = "0.26", features = ["derive"] } thiserror = "1" derive-new = "0.6" num_enum = "0.7" +serde = "1" +serde_json = "1" mio = "0.8" [dependencies.satrs] diff --git a/pytmtc/main.py b/pytmtc/main.py index 9fe494c..a3a69cf 100755 --- a/pytmtc/main.py +++ b/pytmtc/main.py @@ -233,13 +233,13 @@ def main(): raw_logger = RawTmtcTimedLogWrapper(when=TimedLogWhen.PER_HOUR, interval=1) verificator = PusVerificator() verification_wrapper = VerificationWrapper(verificator, _LOGGER, file_logger) - # Create primary TM handler and add it to the CCSDS Packet Handler + # Create primary TM handlers and add it to the CCSDS Packet Handler tm_handler = PusHandler(file_logger, verification_wrapper, raw_logger) ccsds_handler = CcsdsTmHandler(generic_handler=tm_handler) - # TODO: We could add the CFDP handler for the CFDP APID at a later stage. + # TODO: We could add the CFDP handlers for the CFDP APID at a later stage. # ccsds_handler.add_apid_handler(tm_handler) - # Create TC handler + # Create TC handlers seq_count_provider = PusFileSeqCountProvider() tc_handler = TcHandler(seq_count_provider, verification_wrapper) tmtccmd.setup(setup_args=setup_args) diff --git a/src/handlers/camera.rs b/src/handlers/camera.rs new file mode 100644 index 0000000..d0eae69 --- /dev/null +++ b/src/handlers/camera.rs @@ -0,0 +1,284 @@ +/// Device handler implementation for the IMS-100 Imager used on the OPS-SAT mission. +/// +/// from the [OPSSAT Experimenter Wiki](https://opssat1.esoc.esa.int/projects/experimenter-information/wiki/Camera_Introduction): +/// OPS-SAT has a BST IMS-100 Imager onboard for image acquisition. These RGGB images are 2048x1944px in size. +/// +/// There are two ways of taking pictures, with the NMF or by using the camera API directly. +/// +/// As the NMF method is already explained in the NMF documentation we will focus on triggering the camera API. +/// +/// The camera is located on the -Z face of OPS-SAT +/// +/// Mapping between camera and satellite frames: +/// cam body +/// +x -z +/// +y -x +/// +z +y +/// +/// If you look onto Flatsat as in your picture coordinate system for camera it is +/// +/// Z Z pointing inside Flatsat +/// x---> X +/// | +/// | +/// v Y +/// +/// see also https://opssat1.esoc.esa.int/dmsf/files/6/view + +use crate::requests::CompositeRequest; +use derive_new::new; +use log::debug; +use ops_sat_rs::TimeStampHelper; +use satrs::action::{ActionRequest, ActionRequestVariant}; +use satrs::hk::HkRequest; +use satrs::request::{GenericMessage, MessageMetadata, UniqueApidTargetId}; +use satrs::tmtc::PacketAsVec; +use serde::{Deserialize, Serialize}; +use std::io::Error; +use std::process::Command; +use std::sync::mpsc; + +const DEFAULT_SINGLE_CAM_PARAMS: CameraPictureParameters = CameraPictureParameters { + R: 8, + G: 8, + B: 8, + N: 1, + P: true, + E: 2, + W: 1000, +}; + +const BALANCED_SINGLE_CAM_PARAMS: CameraPictureParameters = CameraPictureParameters { + R: 13, + G: 7, + B: 8, + N: 1, + P: true, + E: 2, + W: 1000, +}; + +const DEFAULT_SINGLE_FLATSAT_CAM_PARAMS: CameraPictureParameters = CameraPictureParameters { + R: 8, + G: 8, + B: 8, + N: 1, + P: true, + E: 200, + W: 1000, +}; + +const BALANCED_SINGLE_FLATSAT_CAM_PARAMS: CameraPictureParameters = CameraPictureParameters { + R: 13, + G: 7, + B: 8, + N: 1, + P: true, + E: 200, + W: 1000, +}; + +#[derive(Debug)] +pub enum CameraActionIds { + DefaultSingle = 1, + BalancedSingle = 2, + DefaultSingleFlatSat = 3, + BalancedSingleFlatSat = 4, + CustomParameters = 5, +} + +impl TryFrom for CameraActionIds { + type Error = (); + + fn try_from(value: u32) -> Result { + match value { + value if value == CameraActionIds::DefaultSingle as u32 => { + Ok(CameraActionIds::DefaultSingle) + } + value if value == CameraActionIds::BalancedSingle as u32 => { + Ok(CameraActionIds::BalancedSingle) + } + value if value == CameraActionIds::DefaultSingleFlatSat as u32 => { + Ok(CameraActionIds::DefaultSingleFlatSat) + } + value if value == CameraActionIds::BalancedSingleFlatSat as u32 => { + Ok(CameraActionIds::BalancedSingleFlatSat) + } + value if value == CameraActionIds::CustomParameters as u32 => { + Ok(CameraActionIds::CustomParameters) + } + _ => Err(()), + } + } +} + +// TODO what happens if limits are exceded +#[allow(non_snake_case)] +#[derive(Serialize, Deserialize, new)] +pub struct CameraPictureParameters { + pub R: u8, + pub G: u8, + pub B: u8, + pub N: u8, // number of images, max: 26 + pub P: bool, // .png flag, true converts raw extracted image from camera to a png + pub E: u32, // exposure time in ms, max: 1580, default: 2, FlatSat: 200 + pub W: u32, // wait time between pictures in ms, max: 40000 +} + +#[allow(dead_code)] +#[derive(new)] +pub struct IMS100BatchHandler { + id: UniqueApidTargetId, + // mode_interface: MpscModeLeafInterface, + composite_request_receiver: mpsc::Receiver>, + // hk_reply_sender: mpsc::Sender>, + tm_sender: mpsc::Sender, + stamp_helper: TimeStampHelper, +} + +#[allow(non_snake_case)] +#[allow(dead_code)] +impl IMS100BatchHandler { + pub fn periodic_operation(&mut self) { + self.stamp_helper.update_from_now(); + // Handle requests. + self.handle_composite_requests(); + // self.handle_mode_requests(); + } + + pub fn handle_composite_requests(&mut self) { + loop { + match self.composite_request_receiver.try_recv() { + Ok(ref msg) => match &msg.message { + CompositeRequest::Hk(hk_request) => { + self.handle_hk_request(&msg.requestor_info, hk_request); + } + CompositeRequest::Action(action_request) => { + self.handle_action_request(&msg.requestor_info, action_request); + } + }, + Err(_) => {} + } + } + } + + pub fn handle_hk_request( + &mut self, + _requestor_info: &MessageMetadata, + _hk_request: &HkRequest, + ) { + // TODO add hk to opssat + } + + pub fn handle_action_request( + &mut self, + _requestor_info: &MessageMetadata, + action_request: &ActionRequest, + ) -> std::io::Result<()> { + let param = match CameraActionIds::try_from(action_request.action_id).unwrap() { + CameraActionIds::DefaultSingle => DEFAULT_SINGLE_CAM_PARAMS, + CameraActionIds::BalancedSingle => BALANCED_SINGLE_CAM_PARAMS, + CameraActionIds::DefaultSingleFlatSat => DEFAULT_SINGLE_FLATSAT_CAM_PARAMS, + CameraActionIds::BalancedSingleFlatSat => BALANCED_SINGLE_FLATSAT_CAM_PARAMS, + CameraActionIds::CustomParameters => match &action_request.variant { + ActionRequestVariant::NoData => return Err(Error::other("No Data sent!")), + ActionRequestVariant::StoreData(_) => { + // let param = serde_json::from_slice() + // TODO implement non dynamic version + return Err(Error::other( + "Static parameter transfer not implemented yet!", + )); + } + ActionRequestVariant::VecData(data) => { + let param: serde_json::Result = + serde_json::from_slice(data.as_slice()); + match param { + Ok(param) => param, + Err(_) => { + return Err(Error::other("Unable to deserialize parameters")); + } + } + } + _ => return Err(Error::other("Invalid Action Request Variant!")), + }, + }; + self.take_picture(param) + } + + pub fn take_picture(&mut self, param: CameraPictureParameters) -> std::io::Result<()> { + let mut cmd = Command::new("ims100_testapp"); + cmd.arg("-R") + .arg(¶m.R.to_string()) + .arg("-G") + .arg(¶m.G.to_string()) + .arg("-B") + .arg(¶m.B.to_string()) + .arg("-c") + .arg("/dev/cam_tty") + .arg("-m") + .arg("/dev/cam_sd") + .arg("-v") + .arg("0") + .arg("-n") + .arg(¶m.N.to_string()); + if param.P { + cmd.arg("-p"); + } + cmd.arg("-e") + .arg(¶m.E.to_string()) + .arg("-w") + .arg(¶m.W.to_string()); + + let output = cmd.output()?; + + debug!("{}", String::from_utf8_lossy(&output.stdout)); + + Ok(()) + } + + pub fn take_picture_from_str( + &mut self, + R: &str, + G: &str, + B: &str, + N: &str, + P: &str, + E: &str, + W: &str, + ) -> std::io::Result<()> { + let mut cmd = Command::new("ims100_testapp"); + cmd.arg("-R") + .arg(R) + .arg("-G") + .arg(G) + .arg("-B") + .arg(B) + .arg("-c") + .arg("/dev/cam_tty") + .arg("-m") + .arg("/dev/cam_sd") + .arg("-v") + .arg("0") + .arg("-n") + .arg(N) + .arg(P) + .arg("-e") + .arg(E) + .arg("-w") + .arg(W); + + let output = cmd.output()?; + + debug!("{}", String::from_utf8_lossy(&output.stdout)); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_crc() { + // TODO + } +} \ No newline at end of file diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..b84959a --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1 @@ +pub mod camera; diff --git a/src/main.rs b/src/main.rs index afc0843..4a629be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ use crate::{ }; mod controller; +mod handlers; mod interface; mod logger; mod pus; @@ -247,7 +248,7 @@ fn main() { }) .unwrap(); - info!("Starting PUS handler thread"); + info!("Starting PUS handlers thread"); let pus_stop_signal = stop_signal.clone(); let jh_pus_handler = thread::Builder::new() .name("ops-sat pus".to_string()) @@ -277,5 +278,5 @@ fn main() { .expect("Joining TM Funnel thread failed"); jh_pus_handler .join() - .expect("Joining PUS handler thread failed"); + .expect("Joining PUS handlers thread failed"); } diff --git a/src/pus/mod.rs b/src/pus/mod.rs index 2f88a26..e39c7d1 100644 --- a/src/pus/mod.rs +++ b/src/pus/mod.rs @@ -176,7 +176,7 @@ pub trait TargetedPusService { fn check_for_request_timeouts(&mut self); } -/// This is a generic handler class for all PUS services where a PUS telecommand is converted +/// This is a generic handlers class for all PUS services where a PUS telecommand is converted /// to a targeted request. /// /// The generic steps for this process are the following @@ -186,12 +186,12 @@ pub trait TargetedPusService { /// 3. Convert the PUS TC to a typed request using the [PusTcToRequestConverter]. /// 4. Route the requests using the [GenericRequestRouter]. /// 5. Add the request to the active request map using the [ActiveRequestMapProvider] abstraction. -/// 6. Check for replies which complete the forwarded request. The handler takes care of +/// 6. Check for replies which complete the forwarded request. The handlers takes care of /// the verification process. /// 7. Check for timeouts of active requests. Generally, the timeout on the service level should /// be highest expected timeout for the given target. /// -/// The handler exposes the following API: +/// The handlers exposes the following API: /// /// 1. [Self::handle_one_tc] which tries to poll and handle one TC packet, covering steps 1-5. /// 2. [Self::check_one_reply] which tries to poll and handle one reply, covering step 6. diff --git a/src/pus/test.rs b/src/pus/test.rs index 8428be7..7e7c6fc 100644 --- a/src/pus/test.rs +++ b/src/pus/test.rs @@ -49,7 +49,7 @@ impl TestCustomServiceWrapper { pub fn poll_and_handle_next_packet(&mut self, time_stamp: &[u8]) -> HandlingStatus { let res = self.handler.poll_and_handle_next_tc(time_stamp); if res.is_err() { - warn!("PUS17 handler failed with error {:?}", res.unwrap_err()); + warn!("PUS17 handlers failed with error {:?}", res.unwrap_err()); return HandlingStatus::Empty; } match res.unwrap() { diff --git a/test_pictures/img_msec_1536094659361_2.ims_rgb b/test_pictures/img_msec_1536094659361_2.ims_rgb new file mode 100644 index 0000000..8430879 Binary files /dev/null and b/test_pictures/img_msec_1536094659361_2.ims_rgb differ