diff --git a/Cargo.lock b/Cargo.lock index 2952816..d468c5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,6 +304,95 @@ dependencies = [ "log", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -326,6 +415,20 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "homedir" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22074da8bba2ef26fc1737ae6c777b5baab5524c2dc403b5c6a76166766ccda5" +dependencies = [ + "cfg-if", + "nix", + "serde", + "widestring", + "windows-sys 0.48.0", + "wmi", +] + [[package]] name = "humantime" version = "2.1.0" @@ -410,6 +513,15 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "mio" version = "0.8.11" @@ -422,6 +534,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -482,6 +607,7 @@ dependencies = [ "derive-new", "env_logger", "fern", + "homedir", "humantime", "lazy_static", "log", @@ -516,6 +642,18 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro-crate" version = "3.1.0" @@ -704,6 +842,15 @@ dependencies = [ "serde", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "0.6.14" @@ -932,6 +1079,24 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-implement", + "windows-interface", + "windows-targets 0.52.5", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -941,6 +1106,28 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-implement" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "windows-interface" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1098,6 +1285,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "wmi" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f0a4062ca522aad4705a2948fd4061b3857537990202a8ddd5af21607f79a" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror", + "windows", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 3bd0973..e5d4c9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ num_enum = "0.7" serde = "1" serde_json = "1" mio = "0.8" +homedir = "0.2" socket2 = "0.5" [dependencies.satrs] diff --git a/pytmtc/camera_params.py b/pytmtc/camera_params.py new file mode 100644 index 0000000..2562372 --- /dev/null +++ b/pytmtc/camera_params.py @@ -0,0 +1,31 @@ +import struct +from serde import Model, fields + +from common import EXPERIMENT_APID, UniqueId, make_addressable_id + +class CameraParameters(Model): + R: fields.Int() + G: fields.Int() + B: fields.Int() + N: fields.Int() + P: fields.Bool() + E: fields.Int() + W: fields.Int() + + def serialize_for_uplink(self) -> bytearray: + return self.to_json().encode('utf-8') + +# Example serialization +data = bytearray(make_addressable_id(EXPERIMENT_APID, UniqueId.CameraHandler)) +params = CameraParameters(8, 8, 8, 1, True, 200, 1000) +serialized = params.to_json().encode('utf-8') +byte_string = bytearray(struct.pack('!{}s'.format(len(serialized)), serialized)) +print(byte_string) +print(params.serialize_for_uplink()) +data.extend(params.serialize_for_uplink()) +print(data) + +# Example deserialization +data = '{"R": 100, "G": 150, "B": 200, "N": 3, "P": true, "E": 10, "W": 20}' +deserialized_params = CameraParameters.from_json(data) +print(deserialized_params) diff --git a/pytmtc/common.py b/pytmtc/common.py index 56b469f..52bcbf1 100644 --- a/pytmtc/common.py +++ b/pytmtc/common.py @@ -3,13 +3,30 @@ from __future__ import annotations import dataclasses import enum import struct - +from serde import Model, fields EXPERIMENT_ID = 278 EXPERIMENT_APID = 1024 + EXPERIMENT_ID +class UniqueId(enum.IntEnum): + + Controller = 0 + PusEventManagement = 1 + PusRouting = 2 + PusTest = 3 + PusAction = 4 + PusMode = 5 + PusHk = 6 + UdpServer = 7 + TcpServer = 8 + TcpSppClient = 9 + PusScheduler = 10 + CameraHandler = 11 + + class EventSeverity(enum.IntEnum): + INFO = 0 LOW = 1 MEDIUM = 2 @@ -43,6 +60,13 @@ class AcsHkIds(enum.IntEnum): def make_addressable_id(target_id: int, unique_id: int) -> bytes: - byte_string = bytearray(struct.pack("!I", target_id)) - byte_string.extend(struct.pack("!I", unique_id)) + byte_string = bytearray(struct.pack("!I", unique_id)) + # byte_string = bytearray(struct.pack("!I", target_id)) + # byte_string.extend(struct.pack("!I", unique_id)) + return byte_string + + +def make_addressable_id_with_action_id(unique_id: int, action_id: int) -> bytes: + byte_string = bytearray(struct.pack("!I", unique_id)) + byte_string.extend(struct.pack("!I", action_id)) return byte_string diff --git a/pytmtc/pus_tc.py b/pytmtc/pus_tc.py index 3a8e83d..136d311 100644 --- a/pytmtc/pus_tc.py +++ b/pytmtc/pus_tc.py @@ -9,6 +9,10 @@ from tmtccmd.pus.tc.s200_fsfw_mode import Mode from tmtccmd.tmtc import DefaultPusQueueHelper from tmtccmd.pus.s11_tc_sched import create_time_tagged_cmd from tmtccmd.pus.s200_fsfw_mode import Subservice as ModeSubservice +from serde import Model, fields + +from camera_params import CameraParameters +from common import EXPERIMENT_APID, UniqueId, make_addressable_id, make_addressable_id_with_action_id _LOGGER = logging.getLogger(__name__) @@ -65,6 +69,38 @@ def create_cmd_definition_tree() -> CmdTreeNode: ) root_node.add_child(scheduler_node) + action_node = CmdTreeNode("action", "Action Node") + cam_node = CmdTreeNode( + "take_image", "Take Image with IMS Imager" + ) + cam_node.add_child( + CmdTreeNode( + "default_single", "Default Single Image Camera Parameters" + ) + ) + cam_node.add_child( + CmdTreeNode( + "balanced_single", "Balanced Single Image Camera Parameters" + ) + ) + cam_node.add_child( + CmdTreeNode( + "default_single_flatsat", "Default Single Image Camera Parameters for use on FlatSat" + ) + ) + cam_node.add_child( + CmdTreeNode( + "balanced_single_flatsat", "Balanced Single Image Camera Parameters for use on FlatSat" + ) + ) + cam_node.add_child( + CmdTreeNode( + "custom_params", "Custom Camera Parameters as specified from file" + ) + ) + action_node.add_child(cam_node) + root_node.add_child(action_node) + return root_node @@ -97,6 +133,26 @@ def pack_pus_telecommands(q: DefaultPusQueueHelper, cmd_path: str): ) if cmd_path_list[0] == "acs": assert len(cmd_path_list) >= 2 + if cmd_path_list[0] == "action": + assert len(cmd_path_list)>= 2 + if cmd_path_list[1] == "take_image": + assert len(cmd_path_list)>= 3 + q.add_log_cmd("Sending PUS take image action request with " + cmd_path_list[2] + " params.") + if cmd_path_list[2] == "default_single": + data = make_addressable_id_with_action_id(UniqueId.CameraHandler, 1) + if cmd_path_list[2] == "balanced_single": + data = make_addressable_id_with_action_id(UniqueId.CameraHandler, 2) + if cmd_path_list[2] == "default_single_flatsat": + data = make_addressable_id_with_action_id(UniqueId.CameraHandler, 3) + if cmd_path_list[2] == "balanced_single_flatsat": + data = make_addressable_id_with_action_id(UniqueId.CameraHandler, 4) + if cmd_path_list[2] == "custom": + data = make_addressable_id_with_action_id(UniqueId.CameraHandler, 5) + params = CameraParameters(8, 8, 8, 1, True, 200, 1000) + bytes = params.serialize_for_uplink() + data.extend(bytes) + print(data.hex(sep=",")) + return q.add_pus_tc(PusTelecommand(service=8, subservice=128, apid=EXPERIMENT_APID, app_data=data)) def handle_set_mode_cmd( diff --git a/pytmtc/pyclient.py b/pytmtc/pyclient.py index 4bb0829..aec7024 100755 --- a/pytmtc/pyclient.py +++ b/pytmtc/pyclient.py @@ -147,6 +147,12 @@ class PusHandler(GenericApidHandlerBase): _LOGGER.info(f"Received event packet. Event: {event_u32}") if event_u32.group_id == 0 and event_u32.unique_id == 0: _LOGGER.info("Received test event") + elif service == 8: + if pus_tm.subservice == 130: + _LOGGER.info(f"Received Action Data Reply TM[8,130]") + reply = pus_tm.source_data + reply = reply[6:] + _LOGGER.info(f"Data Reply Content: " + reply.decode('utf-8')) elif service == 17: tm_packet = Service17Tm.unpack( packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE diff --git a/pytmtc/tc_definitions.py b/pytmtc/tc_definitions.py index 74fbff8..acd2741 100644 --- a/pytmtc/tc_definitions.py +++ b/pytmtc/tc_definitions.py @@ -35,4 +35,12 @@ def tc_definitions() -> TmtcDefinitionWrapper: info="PUS Service 11 TC Scheduling", op_code_entry=srv_11, ) + srv_8 = OpCodeEntry() + srv_8.add("pic", "Action Request Image") + defs.add_service( + name=CoreServiceList.SERVICE_8, + info="PUS Service 8 Action", + op_code_entry=srv_8, + + ) return defs diff --git a/src/config.rs b/src/config.rs index fad0b9d..f7a260a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,13 +3,12 @@ use num_enum::{IntoPrimitive, TryFromPrimitive}; use satrs::spacepackets::PacketId; use satrs_mib::res_code::ResultU16Info; use satrs_mib::resultcode; -use std::env; use std::net::Ipv4Addr; use std::path::{Path, PathBuf}; pub const STOP_FILE_NAME: &str = "stop-experiment"; pub const CONFIG_FILE_NAME: &str = "exp278.toml"; -pub const HOME_FOLER_EXPERIMENT: &str = "/home/exp278"; +pub const HOME_FOLDER_EXPERIMENT: &str = "/home/exp278"; pub const LOG_FOLDER: &str = "logs"; pub const OBSW_SERVER_ADDR: Ipv4Addr = Ipv4Addr::UNSPECIFIED; @@ -42,12 +41,17 @@ pub enum GroupId { lazy_static! { pub static ref HOME_PATH: PathBuf = { - let home_path_default = env::var("HOME").expect("HOME env variable not set"); let mut home_path = PathBuf::new(); - home_path.push(if Path::new(HOME_FOLER_EXPERIMENT).exists() { - HOME_FOLER_EXPERIMENT + 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 + home_path_default + .to_str() + .expect("Error converting to string.") }); home_path }; @@ -239,6 +243,7 @@ pub mod components { TcpServer = 8, TcpSppClient = 9, PusScheduler = 10, + CameraHandler = 11, } pub const CONTROLLER_ID: UniqueApidTargetId = @@ -263,6 +268,8 @@ pub mod components { UniqueApidTargetId::new(EXPERIMENT_APID, UniqueId::TcpServer as u32); pub const TCP_SPP_CLIENT: UniqueApidTargetId = UniqueApidTargetId::new(EXPERIMENT_APID, UniqueId::TcpSppClient as u32); + pub const CAMERA_HANDLER: UniqueApidTargetId = + UniqueApidTargetId::new(EXPERIMENT_APID, UniqueId::CameraHandler as u32); } pub mod tasks { diff --git a/src/handlers/camera.rs b/src/handlers/camera.rs index 68d0c61..d136856 100644 --- a/src/handlers/camera.rs +++ b/src/handlers/camera.rs @@ -1,3 +1,4 @@ +use crate::pus::action::send_data_reply; /// 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): @@ -26,17 +27,23 @@ /// see also https://opssat1.esoc.esa.int/dmsf/files/6/view use crate::requests::CompositeRequest; use derive_new::new; -use log::debug; +use log::{debug, info}; use ops_sat_rs::TimeStampHelper; use satrs::action::{ActionRequest, ActionRequestVariant}; use satrs::hk::HkRequest; +use satrs::pus::action::{ActionReplyPus, ActionReplyVariant}; +use satrs::pus::EcssTmtcError; use satrs::request::{GenericMessage, MessageMetadata, UniqueApidTargetId}; use satrs::tmtc::PacketAsVec; use serde::{Deserialize, Serialize}; -use std::io::Error; -use std::process::Command; +use std::fmt; +use std::fmt::Formatter; +use std::process::{Command, Output}; use std::sync::mpsc; +// const IMS_TESTAPP: &str = "scripts/ims100_testapp"; +const IMS_TESTAPP: &str = "ims100_testapp"; + const DEFAULT_SINGLE_CAM_PARAMS: CameraPictureParameters = CameraPictureParameters { R: 8, G: 8, @@ -77,8 +84,12 @@ const BALANCED_SINGLE_FLATSAT_CAM_PARAMS: CameraPictureParameters = CameraPictur W: 1000, }; +// TODO copy as action +// TODO ls -l via cfdp +// TODO howto downlink + #[derive(Debug)] -pub enum CameraActionIds { +pub enum CameraActionId { DefaultSingle = 1, BalancedSingle = 2, DefaultSingleFlatSat = 3, @@ -86,25 +97,25 @@ pub enum CameraActionIds { CustomParameters = 5, } -impl TryFrom for CameraActionIds { +impl TryFrom for CameraActionId { type Error = (); fn try_from(value: u32) -> Result { match value { - value if value == CameraActionIds::DefaultSingle as u32 => { - Ok(CameraActionIds::DefaultSingle) + value if value == CameraActionId::DefaultSingle as u32 => { + Ok(CameraActionId::DefaultSingle) } - value if value == CameraActionIds::BalancedSingle as u32 => { - Ok(CameraActionIds::BalancedSingle) + value if value == CameraActionId::BalancedSingle as u32 => { + Ok(CameraActionId::BalancedSingle) } - value if value == CameraActionIds::DefaultSingleFlatSat as u32 => { - Ok(CameraActionIds::DefaultSingleFlatSat) + value if value == CameraActionId::DefaultSingleFlatSat as u32 => { + Ok(CameraActionId::DefaultSingleFlatSat) } - value if value == CameraActionIds::BalancedSingleFlatSat as u32 => { - Ok(CameraActionIds::BalancedSingleFlatSat) + value if value == CameraActionId::BalancedSingleFlatSat as u32 => { + Ok(CameraActionId::BalancedSingleFlatSat) } - value if value == CameraActionIds::CustomParameters as u32 => { - Ok(CameraActionIds::CustomParameters) + value if value == CameraActionId::CustomParameters as u32 => { + Ok(CameraActionId::CustomParameters) } _ => Err(()), } @@ -113,7 +124,7 @@ impl TryFrom for CameraActionIds { // TODO what happens if limits are exceded #[allow(non_snake_case)] -#[derive(Serialize, Deserialize, new)] +#[derive(Debug, Serialize, Deserialize, new)] pub struct CameraPictureParameters { pub R: u8, pub G: u8, @@ -124,20 +135,89 @@ pub struct CameraPictureParameters { pub W: u32, // wait time between pictures in ms, max: 40000 } +#[derive(Debug)] #[allow(dead_code)] -#[derive(new)] +pub enum CameraError { + TakeImageError, + NoDataSent, + VariantNotImplemented, + DeserializeError, + ListFileError, + IoError(std::io::Error), + EcssTmtcError(EcssTmtcError), +} + +impl From for CameraError { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} + +impl From for CameraError { + fn from(value: EcssTmtcError) -> Self { + Self::EcssTmtcError(value) + } +} + +impl fmt::Display for CameraError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + CameraError::TakeImageError => { + write!(f, "Error taking image.") + } + CameraError::NoDataSent => { + write!(f, "No data sent.") + } + CameraError::VariantNotImplemented => { + write!(f, "Request variant not implemented.") + } + CameraError::DeserializeError => { + write!(f, "Unable to deserialize parameters.") + } + CameraError::ListFileError => { + write!(f, "Error listing image files.") + } + CameraError::IoError(io_error) => { + write!(f, "{}", io_error) + } + CameraError::EcssTmtcError(ecss_tmtc_error) => { + write!(f, "{}", ecss_tmtc_error) + } + } + } +} + +#[allow(dead_code)] +#[derive(Debug)] pub struct IMS100BatchHandler { id: UniqueApidTargetId, // mode_interface: MpscModeLeafInterface, - composite_request_receiver: mpsc::Receiver>, + composite_request_rx: mpsc::Receiver>, // hk_reply_sender: mpsc::Sender>, - tm_sender: mpsc::Sender, + tm_tx: mpsc::Sender, + action_reply_tx: mpsc::Sender>, stamp_helper: TimeStampHelper, } #[allow(non_snake_case)] #[allow(dead_code)] impl IMS100BatchHandler { + pub fn new( + id: UniqueApidTargetId, + composite_request_rx: mpsc::Receiver>, + tm_tx: mpsc::Sender, + action_reply_tx: mpsc::Sender>, + stamp_helper: TimeStampHelper, + ) -> Self { + Self { + id, + composite_request_rx, + tm_tx, + action_reply_tx, + stamp_helper, + } + } + pub fn periodic_operation(&mut self) { self.stamp_helper.update_from_now(); // Handle requests. @@ -147,7 +227,7 @@ impl IMS100BatchHandler { pub fn handle_composite_requests(&mut self) { loop { - match self.composite_request_receiver.try_recv() { + match self.composite_request_rx.try_recv() { Ok(ref msg) => match &msg.message { CompositeRequest::Hk(hk_request) => { self.handle_hk_request(&msg.requestor_info, hk_request); @@ -181,41 +261,50 @@ impl IMS100BatchHandler { pub fn handle_action_request( &mut self, - _requestor_info: &MessageMetadata, + 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")); + ) -> Result<(), CameraError> { + let param = + match CameraActionId::try_from(action_request.action_id).expect("Invalid action id") { + CameraActionId::DefaultSingle => DEFAULT_SINGLE_CAM_PARAMS, + CameraActionId::BalancedSingle => BALANCED_SINGLE_CAM_PARAMS, + CameraActionId::DefaultSingleFlatSat => DEFAULT_SINGLE_FLATSAT_CAM_PARAMS, + CameraActionId::BalancedSingleFlatSat => BALANCED_SINGLE_FLATSAT_CAM_PARAMS, + CameraActionId::CustomParameters => match &action_request.variant { + ActionRequestVariant::NoData => return Err(CameraError::NoDataSent), + ActionRequestVariant::StoreData(_) => { + // let param = serde_json::from_slice() + // TODO implement non dynamic version + return Err(CameraError::VariantNotImplemented); + } + ActionRequestVariant::VecData(data) => { + let param: serde_json::Result = + serde_json::from_slice(data.as_slice()); + match param { + Ok(param) => param, + Err(_) => { + return Err(CameraError::DeserializeError); + } } } - } - _ => return Err(Error::other("Invalid Action Request Variant!")), - }, - }; - self.take_picture(param) + _ => return Err(CameraError::VariantNotImplemented), + }, + }; + let output = self.take_picture(param)?; + debug!("Sending action reply!"); + send_data_reply(self.id, output.stdout, &self.stamp_helper, &self.tm_tx)?; + self.action_reply_tx + .send(GenericMessage::new( + *requestor_info, + ActionReplyPus::new(action_request.action_id, ActionReplyVariant::Completed), + )) + .unwrap(); + Ok(()) } - pub fn take_picture(&mut self, param: CameraPictureParameters) -> std::io::Result<()> { - let mut cmd = Command::new("ims100_testapp"); + pub fn take_picture(&mut self, param: CameraPictureParameters) -> Result { + info!("Taking image!"); + let mut cmd = Command::new(IMS_TESTAPP); cmd.arg("-R") .arg(¶m.R.to_string()) .arg("-G") @@ -237,12 +326,23 @@ impl IMS100BatchHandler { .arg(¶m.E.to_string()) .arg("-w") .arg(¶m.W.to_string()); - let output = cmd.output()?; - debug!("{}", String::from_utf8_lossy(&output.stdout)); + debug!("Imager Output: {}", String::from_utf8_lossy(&output.stdout)); - Ok(()) + Ok(output) + } + + pub fn list_current_images(&self) -> Result, CameraError> { + let output = Command::new("ls").arg("-l").arg("*.png").output()?; + + if output.status.success() { + let output_str = String::from_utf8(output.stdout).unwrap(); + let files: Vec = output_str.lines().map(|s| s.to_string()).collect(); + Ok(files) + } else { + Err(CameraError::ListFileError) + } } #[allow(clippy::too_many_arguments)] @@ -255,7 +355,7 @@ impl IMS100BatchHandler { P: &str, E: &str, W: &str, - ) -> std::io::Result<()> { + ) -> Result<(), CameraError> { let mut cmd = Command::new("ims100_testapp"); cmd.arg("-R") .arg(R) @@ -287,8 +387,85 @@ impl IMS100BatchHandler { #[cfg(test)] mod tests { + use crate::handlers::camera::{ + CameraActionId, CameraPictureParameters, IMS100BatchHandler, + DEFAULT_SINGLE_FLATSAT_CAM_PARAMS, + }; + use crate::requests::CompositeRequest; + 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::request::{GenericMessage, MessageMetadata}; + use satrs::tmtc::PacketAsVec; + use std::sync::mpsc; + use std::sync::mpsc::{Receiver, Sender}; + + fn create_handler() -> ( + IMS100BatchHandler, + Sender>, + Receiver, + Receiver>, + ) { + 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( + CAMERA_HANDLER, + composite_request_rx, + tm_tx, + action_reply_tx, + time_helper, + ); + (cam_handler, composite_request_tx, tm_rx, action_reply_rx) + } + #[test] - fn test_crc() { - // TODO + fn command_line_execution() { + let (mut cam_handler, req_tx, tm_rx, action_reply_rx) = create_handler(); + cam_handler + .take_picture(DEFAULT_SINGLE_FLATSAT_CAM_PARAMS) + .unwrap(); + } + + #[test] + fn serialize_and_deserialize_command() { + let data = serde_json::to_string(&DEFAULT_SINGLE_FLATSAT_CAM_PARAMS).unwrap(); + println!("{}", data); + let param: CameraPictureParameters = serde_json::from_str(&data).unwrap(); + println!("{:?}", param); + } + + #[test] + fn test_action_req() { + let (mut cam_handler, req_tx, tm_rx, action_reply_rx) = create_handler(); + + let data = serde_json::to_string(&DEFAULT_SINGLE_FLATSAT_CAM_PARAMS).unwrap(); + let req = ActionRequest::new( + CameraActionId::CustomParameters as u32, + ActionRequestVariant::VecData(data.as_bytes().to_vec()), + ); + + cam_handler + .handle_action_request(&MessageMetadata::new(1, 1), &req) + .unwrap(); + } + + #[test] + fn test_action_req_channel() { + let (mut cam_handler, req_tx, tm_rx, action_reply_rx) = create_handler(); + + let data = serde_json::to_string(&DEFAULT_SINGLE_FLATSAT_CAM_PARAMS).unwrap(); + let req = ActionRequest::new( + CameraActionId::CustomParameters as u32, + ActionRequestVariant::VecData(data.as_bytes().to_vec()), + ); + let req = CompositeRequest::Action(req); + req_tx + .send(GenericMessage::new(MessageMetadata::new(1, 1), req)) + .unwrap(); + + cam_handler.periodic_operation(); } } diff --git a/src/lib.rs b/src/lib.rs index d637744..014c607 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use satrs::spacepackets::time::TimeWriter; pub mod config; +#[derive(Debug)] pub struct TimeStampHelper { stamper: CdsTime, time_stamp: [u8; 7], diff --git a/src/logger.rs b/src/logger.rs index 4df2e97..8779c13 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use ops_sat_rs::config::LOG_FOLDER; @@ -6,6 +6,9 @@ pub fn setup_logger() -> Result<(), fern::InitError> { if !Path::new(LOG_FOLDER).exists() && std::fs::create_dir_all(LOG_FOLDER).is_err() { eprintln!("Failed to create log folder '{}'", LOG_FOLDER); } + let mut path_buf = PathBuf::from(LOG_FOLDER); + path_buf.push("output.log"); + println!("{:?}", path_buf); fern::Dispatch::new() .format(move |out, message, record| { out.finish(format_args!( @@ -18,11 +21,7 @@ pub fn setup_logger() -> Result<(), fern::InitError> { }) .level(log::LevelFilter::Debug) .chain(std::io::stdout()) - .chain(fern::log_file(format!( - "{}/output_{}.log", - LOG_FOLDER, - humantime::format_rfc3339_seconds(std::time::SystemTime::now()) - ))?) + .chain(fern::log_file(path_buf.as_os_str())?) .apply()?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index f3faaab..046f4fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::{ }; use log::info; +use ops_sat_rs::config::components::CAMERA_HANDLER; use ops_sat_rs::config::{ cfg_file::create_app_config, components::{CONTROLLER_ID, TCP_SERVER, TCP_SPP_CLIENT, UDP_SERVER}, @@ -14,8 +15,10 @@ use ops_sat_rs::config::{ VALID_PACKET_ID_LIST, }; use ops_sat_rs::config::{tasks::FREQ_MS_UDP_TMTC, OBSW_SERVER_ADDR, SERVER_PORT}; +use ops_sat_rs::TimeStampHelper; use satrs::hal::std::{tcp_server::ServerConfig, udp_server::UdpTcServer}; +use crate::handlers::camera::IMS100BatchHandler; use crate::pus::{ hk::create_hk_service, mode::create_mode_service, scheduler::create_scheduler_service, PusTcDistributor, PusTcMpscRouter, @@ -67,12 +70,16 @@ fn main() { let (_pus_mode_reply_tx, pus_mode_reply_rx) = mpsc::channel(); let (controller_composite_tx, controller_composite_rx) = mpsc::channel(); // let (controller_action_reply_tx, controller_action_reply_rx) = mpsc::channel(); + let (camera_composite_tx, camera_composite_rx) = mpsc::channel(); // Some request are targetable. This map is used to retrieve sender handles based on a target ID. let mut request_map = GenericRequestRouter::default(); request_map .composite_router_map .insert(CONTROLLER_ID.id(), controller_composite_tx); + request_map + .composite_router_map + .insert(CAMERA_HANDLER.id(), camera_composite_tx); let pus_router = PusTcMpscRouter { test_tc_sender: pus_test_tx, @@ -166,7 +173,7 @@ fn main() { let mut controller = ExperimentController::new( controller_composite_rx, - pus_action_reply_tx, + pus_action_reply_tx.clone(), stop_signal.clone(), ); @@ -180,6 +187,18 @@ fn main() { ) .expect("creating TCP SPP client failed"); + let timestamp_helper = TimeStampHelper::default(); + let mut camera_handler: IMS100BatchHandler = IMS100BatchHandler::new( + CAMERA_HANDLER, + camera_composite_rx, + tm_funnel_tx.clone(), + pus_action_reply_tx.clone(), + timestamp_helper, + ); + + // Main Task Thread Definitions + + // Main Experiment Control Task info!("Starting CTRL task"); let ctrl_stop_signal = stop_signal.clone(); let jh_ctrl_thread = thread::Builder::new() @@ -193,6 +212,7 @@ fn main() { }) .unwrap(); + // TMTC and UDP Task info!("Starting TMTC and UDP task"); let tmtc_stop_signal = stop_signal.clone(); let jh_udp_tmtc = thread::Builder::new() @@ -210,6 +230,7 @@ fn main() { }) .unwrap(); + // TCP Server Task let tcp_server_stop_signal = stop_signal.clone(); info!("Starting TCP server task"); let jh_tcp_server = thread::Builder::new() @@ -225,6 +246,7 @@ fn main() { }) .unwrap(); + // TCP SPP Client Task // We could also move this to the existing TCP server thread, but we would have to adapt // the server code for this so we do not block anymore and we pause manually if both the client // and server are IDLE and have nothing to do.. @@ -246,6 +268,7 @@ fn main() { }) .unwrap(); + // TM Funnel Task info!("Starting TM funnel task"); let funnel_stop_signal = stop_signal.clone(); let jh_tm_funnel = thread::Builder::new() @@ -258,7 +281,8 @@ fn main() { }) .unwrap(); - info!("Starting PUS handlers thread"); + // PUS Handler Task + info!("Starting PUS handlers task"); let pus_stop_signal = stop_signal.clone(); let jh_pus_handler = thread::Builder::new() .name("ops-sat pus".to_string()) @@ -271,6 +295,20 @@ fn main() { }) .unwrap(); + // Camera Handler Task + info!("Starting camera handler task"); + let camera_stop_signal = stop_signal.clone(); + let jh_camera_handler = thread::Builder::new() + .name("ops-sat camera".to_string()) + .spawn(move || loop { + camera_handler.periodic_operation(); + if camera_stop_signal.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + }) + .unwrap(); + + // Join Threads jh_ctrl_thread .join() .expect("Joining Controller thread failed"); @@ -289,4 +327,7 @@ fn main() { jh_pus_handler .join() .expect("Joining PUS handlers thread failed"); + jh_camera_handler + .join() + .expect("Joining camera handler thread failed"); } diff --git a/src/pus/action.rs b/src/pus/action.rs index 62166e5..4101e4d 100644 --- a/src/pus/action.rs +++ b/src/pus/action.rs @@ -1,6 +1,7 @@ -use log::{error, warn}; +use log::{debug, error, warn}; use ops_sat_rs::config::components::PUS_ACTION_SERVICE; use ops_sat_rs::config::tmtc_err; +use ops_sat_rs::TimeStampHelper; use satrs::action::{ActionRequest, ActionRequestVariant}; use satrs::params::WritableToBeBytes; use satrs::pus::action::{ @@ -13,11 +14,13 @@ use satrs::pus::verification::{ use satrs::pus::{ ActiveRequestProvider, EcssTcAndToken, EcssTcInVecConverter, EcssTmSender, EcssTmtcError, GenericConversionError, PusPacketHandlerResult, PusReplyHandler, PusServiceHelper, - PusTcToRequestConverter, + PusTcToRequestConverter, PusTmVariant, }; use satrs::request::{GenericMessage, UniqueApidTargetId}; use satrs::spacepackets::ecss::tc::PusTcReader; +use satrs::spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader}; use satrs::spacepackets::ecss::{EcssEnumU16, PusPacket}; +use satrs::spacepackets::SpHeader; use satrs::tmtc::PacketAsVec; use std::sync::mpsc; use std::time::Duration; @@ -29,6 +32,8 @@ use super::{ PusTargetedRequestService, TargetedPusService, }; +pub const DATA_REPLY: u8 = 130; + pub struct ActionReplyHandler { fail_data_buf: [u8; 128], } @@ -270,6 +275,26 @@ impl TargetedPusService for ActionServiceWrapper { } } +pub fn send_data_reply( + apid_target: UniqueApidTargetId, + reply_data: Vec, + stamp_helper: &TimeStampHelper, + tm_sender: &TmSender, +) -> Result<(), EcssTmtcError> { + let sp_header = SpHeader::new_from_apid(apid_target.apid); + let sec_header = PusTmSecondaryHeader::new(8, DATA_REPLY, 0, 0, stamp_helper.stamp()); + let mut data = Vec::new(); + data.extend(apid_target.apid.to_be_bytes()); + data.extend(apid_target.unique_id.to_be_bytes()); + data.extend(reply_data); + debug!( + "{}", + String::from_utf8(data.clone()[6..].to_vec()).expect("Error decoding data reply.") + ); + let data_reply_tm = PusTmCreator::new(sp_header, sec_header, &data, true); + tm_sender.send_tm(apid_target.id(), PusTmVariant::Direct(data_reply_tm)) +} + #[cfg(test)] mod tests { use satrs::pus::test_util::{ @@ -432,6 +457,7 @@ mod tests { .send(EcssTcAndToken::new( TcInMemory::Vec(PacketAsVec::new( self.service.service_helper.id(), + //tc.to_vec().unwrap().into(), tc.to_vec().unwrap(), )), accepted_token,